Learn how to build and test apps with the ORM library — without a real backend. This example shows a simple task board you can run locally and test fully using the same schema and mock server. Use it as a reference for structuring your own projects so that development and tests share one source of truth and no live server is required.
The app demonstrates full CRUD for tasks (create, read, update, delete), task comments, and two user roles with separate routes and views: managers get a team dashboard and task management; users get a personal task board and team page. The same ORM schema backs both the in-browser mock API (MSW) and Vitest, so you develop and test against consistent data and behavior.
- React 18 + React Router 7 (data APIs: loaders, actions)
- miragejs-orm — schema, models, collections, factories, seeds, serializers
- MSW (Mock Service Worker) — HTTP interception for dev and tests
- Vitest + Testing Library — unit and integration tests
- Vite — dev server and build
- Material UI (MUI) — UI components and theming
- TypeScript — end-to-end typing with schema-derived types
All test-related code lives under test/ so you can find it quickly and keep app code separate:
test/schema/— ORM schema (models, collections, factories, seeds, serializers). This is the core: it defines your data shape and is used by both the mock server and tests.test/server/— MSW handlers and browser worker setup. Handlers usetestSchemato serve and mutate data.test/context/— Vitest fixture that injects a freshschemaper test and empties data after each test.test/utils/— Test helpers (e.g.renderApp,renderWithRouter, cookie helpers for auth).
Tests for API functions and feature flows live next to the code they cover (see below). The @test/* path alias (e.g. @test/schema, @test/context, @test/server, @test/utils) is configured in tsconfig.json and used consistently in tests and in main.tsx for dev mock server init.
App code is under src/, with feature-based folders. Each feature that talks to the API has an api/ subfolder; test files sit beside the modules they test.
| Feature / route area | Path (concept) | What it does | Where tests live |
|---|---|---|---|
| auth | /auth |
Login / logout | src/features/auth/api/login.test.ts, logout.test.ts, Login.test.tsx, components/LoginForm.test.tsx |
| app-layout | / (layout + redirect) |
Layout, user loader, role-based redirect | src/features/app-layout/api/getUser.test.ts, AppLayout.test.tsx, component tests in components/ |
| dashboard (manager) | /:teamName/dashboard |
Team tasks table, stats, filters | src/features/dashboard/api/getTeamTasks.test.ts, getTaskStatistics.test.ts, Dashboard.test.tsx, and tests under components/ |
| user-board (user) | /:teamName/users/:userId |
User’s task list by status | src/features/user-board/api/getUserTasks.test.ts, UserBoard.test.tsx, components/ |
| task-details | .../tasks/:taskId or .../:taskId |
Task view (manager or user context) | src/features/task-details/api/getTaskDetails.test.ts, TaskDetails.test.tsx, components/ |
| task-form | .../tasks/:taskId (form) |
Create/update task | src/features/task-form/api/createTask.test.ts, updateTask.test.ts, TaskForm.test.tsx, components/ |
| delete-task | .../tasks/:taskId/delete |
Delete task | src/features/delete-task/api/deleteTask.test.ts |
| task-comments | Nested under task details | Load/add comments | src/features/task-comments/api/getTaskComments.test.ts, addTaskComment.test.ts, TaskComments.test.tsx, components/ |
| team (user) | /:teamName/users/:userId/team |
Team info and members | src/features/team/api/getTeam.test.ts, getTeamMembers.test.ts, Team.test.tsx, components/ |
Routes and role requirements are defined in src/routes.tsx: manager routes use requiresRole: 'MANAGER', user routes use requiresRole: 'USER'. The same schema backs both.
src/shared/— Shared types, enums (e.g.UserRole,TaskStatus,TaskPriority), reusable components (e.g.ErrorBoundary), and small helpers (e.g. task form defaults, formatting). These are used by features and by the schema (e.g. enums and types for model attrs and JSON).- They are supporting pieces; the main learning focus is the schema and test/server setup below.
The test/schema/ folder and how it is wired into the app and tests is the main pattern to copy. It gives you:
- One schema used in development (MSW) and in tests (Vitest).
- Typed models and collections with relationships, factories, and seeds.
- Controlled test data: each test gets a fresh schema instance and an empty DB after the test (via the
testfixture intest/context/context.ts).
test/
schema/
schema.ts # Builds the single schema instance and exports TestSchema / TestCollections
index.ts # Re-exports for @test/schema
models/ # One file per entity: attrs + model() definition
collections/ # One folder per entity: collection, factory, relations, serializer, seeds
-
schema/schema.ts- Calls
schema()frommiragejs-orm, registers all collections, sets logging, and calls.build(). - Exports
testSchema(the instance),TestSchema, andTestCollections(for typing collections and factories).
- Calls
-
schema/models/- Each model file defines attributes (e.g.
UserAttrs,TaskAttrs) and builds a model withmodel().name(...).collection(...).attrs<T>().json<T>().build(). - Models reference shared app types (e.g.
User,Task) for the.json<T>()shape so API and UI types stay aligned.
- Each model file defines attributes (e.g.
-
schema/collections/<entity>/
For each entity (e.g.users,tasks,teams,comments):- Collection:
collection<TestCollections>().model(...).factory(...).relationships(...).serializer(...).seeds(...).build(). - Relationships:
relations.belongsTo/relations.hasManywith correctforeignKey(and optionalinverse) so the ORM can traverse associations. - Factory:
factory<TestCollections>().model(...).attrs({ ... }).traits({ ... })for creating test data (and optional seed data) with Faker or fixed values. - Serializer: Default and/or named serializers (e.g.
taskItemSerializer,userInfoSerializer) to control what is returned by the API and in tests. - Seeds: A
.seeds((schema) => { ... })function that creates the default dataset. Seed order matters when entities depend on each other (e.g. teams → users → tasks → comments).
- Collection:
-
Development
Insrc/main.tsx, whenimport.meta.env.DEVis true, the app callsinitMockServer()from@test/server. That loads seeds withtestSchema.loadSeeds({ onlyDefault: true })and starts the MSW worker. The sametestSchemais used by all MSW handlers intest/server/handlers/tofind,create,update,delete, and serialize data. So the app runs against the ORM without a real server. -
Tests
API and integration tests use thetestfixture from@test/context. That fixture provides aschemaargument (the sametestSchemainstance). Tests use it to create data (e.g.schema.users.create(),schema.tasks.create('todo', { ... })) and assert on results (e.g.schema.tasks.find(id).toJSON()). After each test,testSchema.db.emptyData()runs so the next test starts clean. MSW handlers are the same ones used in dev, so the same schema backs both.
- Single schema in
test/schema/schema.ts, built from models and collections undertest/schema/. - Path alias
@test/*so app and tests import schema and server fromtest/without relative path clutter. - Seeds only in collections; load them once in dev via
testSchema.loadSeeds({ onlyDefault: true })intest/server/browser.ts. - Test isolation: use the
testfixture from@test/contextso every test gets the same schema and a clean DB after the run. - Handlers in
test/server/handlers/use onlytestSchema(and optional serializers fromtest/schema/collections/...) so behavior is consistent between dev and tests.
Following this structure lets you develop and test the task board (and your own apps) against the ORM without a real server, with one place to define and change data shape and behavior.
From the examples/task-board directory:
pnpm start— Start the dev server (mock server with seeds runs in the browser).pnpm test— Run tests once.pnpm test:watch— Run tests in watch mode.pnpm check:all— Lint, type-check, and format check.pnpm fix:all— Auto-fix format and lint issues.