A NestJS API and a Next.js frontend are a common full-stack pairing, and one I've used on production work. The classic failure: the backend renames a field, TypeScript is happy on both sides, and production breaks. Here's how to make the compiler catch it.
Option 1: OpenAPI codegen
The backend describes itself; the frontend generates types.
Backend: decorate DTOs with @nestjs/swagger and expose the spec.
export class ProjectDto { @ApiProperty() id!: string; @ApiProperty() name!: string; @ApiProperty({ enum: ["active", "archived"] }) status!: "active" | "archived"; }
Frontend: generate types from the spec with openapi-typescript, and use a typed client like openapi-fetch.
import createClient from "openapi-fetch"; import type { paths } from "./api-types"; const api = createClient<paths>({ baseUrl: process.env.API_URL }); const { data, error } = await api.GET("/projects/{id}", { params: { path: { id } }, });
Best for: separate repos, multiple consumers, public APIs.
Option 2: Shared Zod schemas (monorepo)
Put schemas in a shared package. The backend validates with them; the frontend infers types from them.
// packages/contracts/project.ts export const Project = z.object({ id: z.string().uuid(), name: z.string().min(1), status: z.enum(["active", "archived"]), }); export type Project = z.infer<typeof Project>;
Use a Zod validation pipe in NestJS, and parse responses on the client for critical data.
Best for: monorepos where one team owns both sides.
Option 3: Contract libraries
Libraries like ts-rest define the whole API (routes, methods, bodies, responses) as one contract that both NestJS and the client implement.
Best for: end-to-end type safety with minimal codegen.
Which I'd pick
| Situation | Choice |
|---|---|
| Separate repos or external consumers | OpenAPI codegen |
| Monorepo, one team | Shared Zod or ts-rest |
| Lots of existing Swagger docs | OpenAPI codegen |
Whatever you choose, run the type check in CI on both sides so contract drift fails the build, not production.