NestJS and Next.js boilerplate: two apps, one repo, one API contract
CrabStack pairs a NestJS 12 backend with a Next.js 16 frontend in one
repository. Each half is its own application with its own package.json.
An OpenAPI spec connects them, and the frontend's API types are generated
from it.
This page explains how the two halves connect and what keeps them in step when an AI agent changes one of them.
Why two applications
Next.js can serve an API on its own. A separate NestJS backend is worth the extra process when the backend has rules of its own: billing, background workers, admin tools, account deletion. Those run on port 4000 whether the frontend is up or not. The frontend on port 3000 renders pages and calls the backend over REST.
The split also gives an agent a clear boundary. Backend conventions live
in backend/CLAUDE.md, frontend conventions in frontend/CLAUDE.md, and
the agent reads the one for the half it touches.
How the halves connect
- NestJS controllers and zod schemas produce the API description, through
@nestjs/swaggerandnestjs-zod. - That description is written to
openapi.jsonat the repo root. It is the single source of truth for the API. npm run generate-typesinfrontend/turns the spec intotypes/api.d.tswithopenapi-typescript.- The frontend calls the backend through
openapi-fetch, typed by those paths. A wrong path, a missing field or a renamed property is a type error before it is a runtime error.
Nobody edits openapi.json or types/api.d.ts by hand. Both are
regenerated after a backend change and committed with it.
What CI checks
CI builds the spec from the backend code as it stands, and diffs it
against the committed openapi.json. Any difference fails the build. A
second step regenerates the frontend types and fails if they differ from
the committed files. So a backend change that skipped regeneration cannot
merge, and neither can frontend types that drifted from the spec.
How the frontend calls the backend
Pages are React Server Components. They fetch with serverApi(), which
reads the session cookie on the server and attaches the bearer token.
Forms submit to Server Actions in a colocated actions.ts. The bearer
token never reaches the browser.
lib/api.ts imports server-only. A client component that imports from
it fails the build. An agent that tries to call the API from the browser
with a token gets a build error.
There is no TanStack Query or SWR. Server Components and Server Actions cover the data flow.
Running both halves
./setup.sh installs both halves and writes both .env files.
docker compose up -d frontend backend maildev runs the app on port 3000,
the API on 4000 and a local mail inbox. Postgres runs on 5433, so it does
not collide with a Postgres you already run on 5432.
On the word monorepo
The repository holds both applications, so it answers searches for a
NestJS Next.js monorepo. It has no monorepo tool: no Turborepo, Nx or
package-manager workspaces. The halves share no TypeScript package; the
OpenAPI spec is what they share. npm is the only supported package
manager, with a committed package-lock.json in each half.
Who it fits
- You want the backend to be its own deployable service from day one.
- You want frontend types that cannot silently fall out of step with the API.
- You build with an agent and want each half's rules written down where it will read them.
Who it does not fit
- You want one Next.js app with API routes and no second process.
- You want a Turborepo with shared packages between apps.
- You want Nuxt, Remix or TanStack Start. The frontend is Next.js.
- You want GraphQL or tRPC. The API is REST with OpenAPI.
Related
- NestJS SaaS boilerplate: everything that ships
- NestJS Postgres boilerplate: the data layer behind the API
- Claude Code SaaS boilerplate: the agent files for each half
- Vibe coding SaaS template: the checks that catch agent mistakes
- Docs: How the two projects connect, Auth flow, Adding a Module
- Compare: vs ShipFast, vs Makerkit, vs supastarter
FAQ
Is this a NestJS Next.js monorepo?
It is one repository with two applications, backend/ and frontend/,
each with its own package.json. It uses no monorepo tool and no shared
package.
How do frontend types stay in sync with the backend?
The backend produces openapi.json, and npm run generate-types turns it
into types/api.d.ts. CI fails if either file differs from what the code
produces.
Can the browser call the NestJS API directly? Not in the shipped pattern. Server Components and Server Actions call the backend on the server, and the bearer token stays there. Importing the server API client into a client component fails the build.
Which versions are used? NestJS 12, Next.js 16 with the App Router, React 19, TypeScript 6, zod 4, Knex 3, openapi-fetch and openapi-typescript 7. Node 24 and Node 26 are both supported, and CI runs both.
Can I deploy the two halves separately?
Yes. They are separate applications with their own Docker images. The
deploy/ kit runs both behind Caddy on one server.