Type-Safe API Contracts Between NestJS and Next.js

Suman SharmaSuman Sharma

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

SituationChoice
Separate repos or external consumersOpenAPI codegen
Monorepo, one teamShared Zod or ts-rest
Lots of existing Swagger docsOpenAPI codegen

Whatever you choose, run the type check in CI on both sides so contract drift fails the build, not production.

Frequently asked questions

Why not just share TypeScript interfaces?

Interfaces disappear at runtime, so they can't validate real responses. They also drift when the backend changes without the frontend noticing. Generated or schema-based contracts keep both sides honest.

Which approach is best?

OpenAPI codegen suits separate repos and public APIs. Shared Zod schemas suit monorepos. Contract libraries like ts-rest give end-to-end types with less ceremony when you control both sides.

Do I still need runtime validation?

Yes, at the boundaries: validate incoming requests on the server, and ideally validate critical responses on the client.

Contact

Get in Touch

Want to call me? book a call and I'll respond in the available time slot. I will ignore all soliciting.