Contract-first route tooling for the Next.js App Router with compile-time response typing and OpenAPI 3.1 generation.
- Define pure route contracts with
defineContract(...) - Bind Next.js handlers with
bindContract(contract, handler) - Validate params, query, and body at runtime
- Enforce response status/content/body correctness at compile time via typed
respondhelpers - Generate OpenAPI 3.1 from
src/app/apicontract files - Run CLI with zero required flags (
openapi-next)
bun add @nivalis/openapi-next
# or
pnpm add @nivalis/openapi-nextThis package declares
nextandtypescriptas peer dependencies. Use versions already installed in your app.
// src/app/api/users/contract.ts
import { defineContract } from "@nivalis/openapi-next";
import { z } from "zod";
export const listUsersContract = defineContract({
method: "GET",
operationId: "listUsers",
input: {
query: z.object({ page: z.coerce.number().int().min(1).default(1) }),
},
responses: {
200: {
description: "Users list",
content: {
"application/json": {
schema: z.object({
success: z.literal(true),
items: z.array(
z.object({
id: z.string().uuid(),
email: z.string().email(),
}),
),
}),
},
},
},
},
});// src/app/api/users/route.ts
import { bindContract } from "@nivalis/openapi-next";
import { listUsersContract } from "./contract";
export const GET = bindContract(listUsersContract, async ({ query }, respond) =>
respond.json(200, {
success: true,
items: await fetchUsers(query.page),
}),
);Use the CLI directly:
openapi-nextThis command resolves metadata in this order:
- CLI flags (
--title,--version,--description) package.json(name,version,description)- Fallback defaults (
API,0.1.0, empty description)
Useful options:
--app-dir <path>(defaultsrc/app/api)--output <path>(defaultpublic/openapi.json)--strict-missing-contracts
You can also generate from a script:
import { generateOpenapiSpec } from "@nivalis/openapi-next";
await generateOpenapiSpec({
title: "Example API",
version: "1.0.0",
});If Turbopack fails while bundling @nivalis/openapi-next with an error like:
Module not found: Can't resolve './extension-resolver-loader.ts'
add this to your next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["@nivalis/openapi-next"],
};
export default nextConfig;This keeps Next from bundling the package internals during app build.
See docs/migrations/v3.md for breaking changes and migration examples.
- Optional runtime response validation mode for debugging and stricter runtime guarantees.
MIT © Nivalis Studio