# A RealWorld Medium clone built as a page-scoped FSD app

> yurisldk/realworld-react-fsd is an educational React and TypeScript implementation of the RealWorld Medium clone that applies Feature-Sliced Design ideas to a three folder structure. It ships React Router 7 loaders, React Query data fetching, Zod validation, Orval generated API clients and a containerised nginx build, and it openly describes itself as a page-scoped tree rather than a full FSD one.

**yurisldk/realworld-react-fsd** — A real-world React application built with Feature-Sliced Design, evolving from a modular monolith toward a microfrontend-ready architecture.

- Repository: https://github.com/yurisldk/realworld-react-fsd
- Stars: 533 · Forks: 113
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/yurisldk-realworld-react-fsd

## An educational Medium clone, not a product template

The project implements the RealWorld application, the well known Medium clone specification, with React, TypeScript, React Router, React Query and Zod. Its stated purpose is educational and demonstrational: an implementation of Feature-Sliced Design combined with modern frontend tooling, described as suitable for learning, experimentation and as a template for large scale applications.

The audience is therefore two groups at once, and they are not the same. Learners get a working Medium clone with auth, articles, comments, profiles, favourites and follows, all of which come from the RealWorld spec, with a dependency structure that shows how a layered architecture looks when applied to real screens. Working teams get a starting point with the plumbing already solved: a routed data layer, generated API bindings, a test harness and a container image.

The framing matters for how you read the rest of the code. This is a repository that tries to demonstrate a way of organising a frontend, and it is honest about where the demonstration stops, which is unusual and useful.

## Three folders, and an admission that it is not the full FSD tree

The top of the source tree holds three directories. src/app is the application shell, holding the root router, layout, providers and root error handling. src/pages holds page scoped route modules and the page UI. src/shared holds the reusable API layer, utilities, router helpers and shared UI. Everything else in the repository supports those three: src/shared/api/generated is where the Orval output lands, and convert-zod-mini.ts at the root post-processes it.

The architecture notes are unusually direct about the gap between this and the canonical approach. The codebase uses a page scoped structure rather than a full multi-layer FSD tree, and the dependency graph in the repository is there to make the resulting import rules visible. Full Feature-Sliced Design adds layers such as entities, widgets and segments, and this project does not have them.

That is a deliberate simplification, and it cuts both ways. You get a structure that is easy to hold in your head and cheap to route around, and you give up the layer separation that FSD advocates argue prevents UI code from reaching into another feature's internals. The project's own topic list includes both feature-sliced and modular-monolith terms, and the description says it is evolving from a modular monolith toward a microfrontend-ready architecture, so the current tree is a waypoint rather than a final answer.

## Loaders for reads, actions for writes, Zod in between

The data flow is where this repository is most worth reading. The root router is defined in src/app/browser-router.tsx, and each page exposes a route object from src/pages/*/*.route.ts. Route modules are lazy-loaded, so page components, loaders and actions arrive on demand and the initial bundle stays smaller.

Reads happen in route loaders backed by React Query, which means the query cache and the router share one data layer rather than living in two places. Writes happen in route actions, and the stated rule is that route actions usually validate formData with Zod before calling the API. The validation is not incidental: it is the guard that stops malformed payloads leaving the browser, and it is where a Zod schema per form replaces ad hoc checks.

Optimistic state is handled through fetchers, and the feature list names follow and favourite as the interactive actions that use it. The root router renders a dedicated fallback for route and auth failures, so a broken page or an expired session produces a defined screen rather than a blank window.

## The API layer is generated, then rewritten to zod/mini

None of the API bindings are written by hand. The repository ships openapi.yaml and orval.config.ts, and the generate script runs Orval and then a Zod conversion step, with a separate zod:mini script that executes convert-zod-mini.ts through ts-node. A third script, format:generated, runs ESLint and Prettier over src/shared/api/generated so the generated output passes the same checks as hand written code.

The two stage generation is the interesting part. Orval produces TypeScript clients and schemas from the OpenAPI document, and then a local conversion step rewrites the resulting Zod artifacts into the smaller zod/mini variant, which trades some API surface for a smaller runtime payload. The result is a client layer that is regenerated rather than maintained, so a backend contract change is a pull request, not a refactor.

Auth fits the same generated pattern. The auth middleware restores the current user and protects auth-only flows, which means a session check is a router concern rather than something every component has to perform. The request and response shapes behind that middleware come from the same generated layer.

## Running the app locally against a separate API

The local workflow is short. Dependencies are installed with yarn, and the development server is started with the same tool:

```bash
yarn install
```

```bash
yarn start
```

The start script runs webpack serve in development mode, and the documentation lists two endpoints afterwards: the frontend at http://localhost:30401 and the backend API at http://localhost:30400/api. The backend is not part of this repository. The README points at node-express-realworld-example-app for backend integration, so a local run means running that service as well.

The other scripts cover the rest of the loop. yarn test runs Jest, yarn eslint lints and auto-fixes files under src, yarn prettier formats the repository, and yarn build:prod produces the production bundle while yarn analyze:prod produces it with the bundle analyzer enabled. The bundle analyze image in the repository is the visual output of that last script.

## The compose file starts only the frontend, and the image ends at nginx

The container path is worth reading because it shows what is generated at build time. The Dockerfile is a two stage build on node:22-alpine. The builder copies package.json and yarn.lock, installs with a frozen lockfile, then copies the TypeScript and Webpack configuration, openapi.yaml, orval.config.ts and convert-zod-mini.ts, and runs yarn generate followed by yarn build:prod. The API client is therefore regenerated inside the image rather than committed.

The second stage is nginx:alpine, which receives the build directory and nginx.conf. The image carries OpenContainers labels for the source, url, documentation, version, created date and revision, and the licence label reads MIT.

The compose file, though, starts only the frontend service, and it takes the API URL as a build argument:

```yaml
services:
  frontend:
    build:
      context: .
      args:
        API_URL: ${API_URL}
    image: ${IMAGE_NAME:-realworld-frontend:local}
    ports:
      - '${WEB_SERVER_PORT_EXTERNAL}:${WEB_SERVER_PORT_INTERNAL}'
```

The backend is expected separately at http://localhost:30400. The published images are on GitHub Container Registry, tagged by version and latest, and they are built with API_URL pointing at http://localhost:30400/api by default, which is a local address baked into a container image and worth overriding for any real deployment.

## Dependency rules, GraphViz, and where the FSD idea stops

Enforcement is a first class part of the repository. A .dependency-cruiser.js file sits at the root, and the graph script pipes dependency-cruiser output through the GraphViz dot command to produce dependency-graph-preview.svg. The documentation notes that this assumes the dot command is available on most Linux and comparable systems, and points at the GraphViz download page otherwise. The graph image in the README is therefore not decoration, it is the output of a check you can reproduce.

The toolchain has a clear bias. Webpack 5 with Webpack Dev Server, Babel, Jest with Testing Library, MSW for mocking, ESLint and Prettier for hygiene, and Husky hooks configured for pre-commit and pre-push checks. Orval generates the API layer, Zod 4 validates it. Sass handles styling. It is a conventional, explicit setup rather than a minimal one, which suits a teaching repository and adds weight to a real project.

The honest limit is the mismatch between the name and the tree. The topic tags claim feature-sliced design, the description says microfrontend-ready, and the source has app, pages and shared. Nothing in the repository performs a microfrontend split, and there is no segments or entities layer, so anyone who needs strict FSD boundaries will have to add them, along with the import rules that keep them honest.

## Release cadence, licence, and the cost of a working dependency set

The release history is short and mostly recent. There is v1.2.1 from 2025-06-29, v2.0.0 from 2026-04-25 and v2.1.0 from 2026-05-25. The repository is not archived and the last push was on 2026-05-25, so the project is being worked on, and the jump from 1.x to 2.0.0 in April 2026 suggests the structure changed in a way the author considered a major version.

The licence is MIT, which allows you to use, modify and ship the code commercially, with the requirement that the copyright notice and licence text stay with the source. The images and the bundled logos are a separate question the licence text does not settle for you.

The upgrade cost sits in the dependency set rather than in the licence. React 19, React Router 7, TanStack React Query 5 and Zod 4 are all pinned at recent major versions, and the generated API layer is coupled to openapi.yaml, so a backend change is a schema change, a regeneration and a review of the diff. The practical upside is that a scaffolded project like this saves you from wiring the router, the query cache, the validation boundary and the API generation yourself, and the practical downside is that you inherit all of those choices, including the Webpack 5 bundler and the Jest runner, before you change anything.

## Conclusion

Use yurisldk/realworld-react-fsd as a reference implementation if you want to see router driven data loading, Zod validated mutations and generated API clients inside a real, non trivial React application, and if a page-scoped layout suits your team better than a full FSD tree. Do not adopt it as your production starting point if you expect the microfrontend split that the project description only gestures at, since nothing in the repository performs that split. Before you build on it, run yarn graph to see the dependency rules the project enforces, and confirm that the separate node-express-realworld-example-app backend is what you want to point API_URL at.

## FAQ

### Does realworld-react-fsd implement the full Feature-Sliced Design structure?

No. The project documentation states that the codebase uses a page-scoped structure rather than a full multi-layer FSD tree, and the source is organised as src/app, src/pages and src/shared. The description says it is evolving toward a microfrontend-ready architecture.

### How do I run the realworld-react-fsd frontend locally?

Run yarn install and then yarn start, which serves the frontend at http://localhost:30401. The backend API is expected separately at http://localhost:30400/api, and the README points at node-express-realworld-example-app for that side.

### How is the API client code generated in realworld-react-fsd?

The generate script runs Orval against openapi.yaml and then runs the zod:mini step, which executes convert-zod-mini.ts to rewrite the generated Zod artifacts. A separate format:generated script runs ESLint and Prettier over src/shared/api/generated.

### What testing and linting does the project set up?

Jest with Testing Library runs the tests, MSW is used for mocking, ESLint and Prettier handle linting and formatting, and Husky hooks are installed for pre-commit and pre-push checks. yarn test, yarn eslint and yarn prettier are the entry points.

### How do I containerise the realworld-react-fsd frontend?

The Dockerfile builds with yarn generate and yarn build:prod in a node:22-alpine stage and serves the output from nginx:alpine. You can start it with docker compose --env-file .env.compose up -d --build, and the API_URL build argument points the client at the backend.

## Sources

- [Issues](https://github.com/yurisldk/realworld-react-fsd/issues)
- [License: MIT](https://github.com/yurisldk/realworld-react-fsd/blob/master/LICENSE)
- [README](https://github.com/yurisldk/realworld-react-fsd/blob/master/README.md)
- [Releases](https://github.com/yurisldk/realworld-react-fsd/releases)
- [yurisldk/realworld-react-fsd on GitHub](https://github.com/yurisldk/realworld-react-fsd)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/yurisldk-realworld-react-fsd
