Next.js
Mount a Kizuna API on a Next.js App Router catch-all route.
@kizunajs/next serves a Kizuna API from a Next.js App Router application, through a single catch-all route file.
Requires Next.js ≥ 16 (App Router).
pnpm add @kizunajs/next@betabun add @kizunajs/next@betanpm install @kizunajs/next@betaThe adapter
nextAdapter() is what you name in kizuna.config.ts. Naming it is what gives every handler on this API Next's own NextRequest, typed.
Declare a route
import { k } from '../k';
export const users = k.routes('users', {
listUsers: k
.route({
method: 'GET',
path: '/users',
query: ListUsersQuerySchema,
responses: {
200: UserListSchema,
},
})
.handler(async ({ query }) => ({
status: 200,
body: {
users: await db.users.findMany({
skip: (query.page - 1) * query.limit,
take: query.limit,
}),
total: await db.users.count(),
},
})),
getUser: k
.route({
method: 'GET',
path: '/users/:id',
responses: {
200: UserSchema,
404: ProblemDetailsSchema,
},
})
.handler(async ({ params }) => {
const user = await db.users.findById(params.id);
if (!user) {
return {
status: 404,
body: {
detail: 'Not found',
},
};
}
return {
status: 200,
body: user,
};
}),
});Name Next in your config
import { defineConfig } from 'kizunajs';
import { nextAdapter } from '@kizunajs/next';
import { users } from './src/routes/users';
export default defineConfig({
adapter: nextAdapter(),
routes: {
users,
},
});Mount it at a catch-all route
mount hands back a handler per HTTP method, which Next dispatches for you:
import kizuna from '../../../../kizuna.config';
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = kizuna.api.mount({
basePath: '/api',
});Handler context
Each handler receives { params, query, body, headers, request }. The request is the native NextRequest, useful when you need to read cookies or anything else Next exposes.
.handler(async ({ params, request }) => {
const theme = request.cookies.get('theme')?.value;
return {
status: 200,
body: await db.users.findById(params.id),
};
});Guards
A guard sits on the identity it authenticates, and receives the credential extracted and typed, alongside the native request. See the Authentication guide.
import { k } from './k';
export const user = k.identity
.bearer({
context: z.object({
userId: z.string(),
}),
})
.guard(async ({ bearer, deny }) => {
const session = bearer ? await verifySession(bearer.token) : undefined;
if (!session) {
return deny({
status: 401,
body: {
detail: 'Unauthorized',
},
});
}
return {
userId: session.userId,
};
});Middleware
For request-scoped values handlers need, use request context. For cache headers, declare a cache policy on the route. For other middleware, such as logging and rate limiting, use Next.js's own middleware, or the requestMiddleware option to run middleware after route matching. Authentication belongs in guards.
Options
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = kizuna.api.mount({
basePath: '/api',
responseValidation: false,
});| Option | Default | Description |
|---|---|---|
basePath | '' | Path prefix to strip when matching incoming paths |
requestMiddleware | [] | Middleware run after route matching, before the handler. See requestMiddleware |
responseValidation | false | Validate handler return values against response schemas. Enable in development. |
onError | none | Map a thrown error into a response (inbound migration seam); return an AdapterResult/Response to override the default 500. |
onError
Pass onError to nextAdapter(), or to mount, to send your own response:
export default defineConfig({
adapter: nextAdapter({
onError: (error) => {
if (error instanceof AuthError) {
return {
kind: 'raw-response',
response: new Response(
JSON.stringify({
message: 'Unauthorized',
}),
{
status: 401,
headers: {
'Content-Type': 'application/json',
},
}
),
};
}
},
}),
routes,
});requestMiddleware
requestMiddleware applies a flat array of middleware to every matched route, after route matching and before the handler. Each middleware receives (request, route), where route is the matched route's path and method, and may return a Response to short-circuit.
Type-safe request properties
Middleware that sets custom properties on the request, such as a request id, needs module augmentation to extend NextRequest so handlers reach them without casts. Authentication data does not: guards hand handlers typed context directly.
import 'next/server';
declare module 'next/server' {
interface NextRequest {
requestId: string;
}
}Both middleware and handlers are then typed:
.handler(async ({ params, request }) => {
console.log(request.requestId);
// ...
});Multipart / file uploads
Routes with contentType: 'multipart/form-data' work natively. The adapter calls request.formData() internally.
.handler(async ({ body }) => {
await storage.upload(body.userId, body.file);
return {
status: 200,
body: {
size: body.file.size,
userId: body.userId,
},
};
});Streams
A stream response is returned as a NextResponse with a ReadableStream body. The generator's signal is request.signal.