next-route-kit
v0.2.0
Published
Composable Route Handler infrastructure for Next.js App Router
Maintainers
Readme
next-route-kit
The Next.js App Router entry package.
npm install next-route-kitStart with the English user guide, 简体中文指南, or the repository README.
5-minute integration
Add the shared route foundation to any JSON endpoint. No next.config.ts
registration is required.
// app/api/resources/route.ts
import { createRoute, jsonBody } from 'next-route-kit'
const route = createRoute()
export const POST = route({
body: jsonBody<{ name: string }>(),
handler: (_request, { body }) => ({ resource: { name: body.name } }),
})request remains the native Web Request, and special endpoints can stay plain
Next.js handlers. Existing applications can migrate route by route; see the
migration guide
for the complete mapping.
Native route API
import { createRoute, jsonBody, query } from 'next-route-kit'
type ResourceParams = { id: string }
type UpdateInput = { title?: string }
const route = createRoute({
guards: [requireUser],
})
export const GET = route<ResourceParams>({
handler: async (request, { params, locals }) => {
return resourceService.find(params.id, locals.userId)
},
})
export const PATCH = route<ResourceParams, UpdateInput>({
body: jsonBody<UpdateInput>(),
handler: async (_request, { params, body, locals }) => {
return resourceService.update(params.id, locals.userId, body)
},
})The handler is always (request, context). request is the native Web
Request; context.params contains Next dynamic params and
context.locals contains request-local values written by middleware or guards.
Declare body or query only when automatic resolution is useful:
export const POST = route({
body: jsonBody<{ name: string }>(),
query: query<{ preview?: string }>(),
handler: (_request, { body, query: values }) => ({
name: body.name,
preview: values.preview === 'true',
}),
})Raw headers, URL, streaming bodies, files, and special responses remain on the
native Request/Response boundary.
Factory scopes
const apiRoute = createRoute({ middleware, interceptors, exceptionFilters })
const authenticatedRoute = apiRoute.withLocals(sessionProvider)
const adminRoute = authenticatedRoute.extend({ guards: [requireAdmin] })extend() returns a new immutable scope. It does not mutate the parent and
does not require a next.config.ts registration.
Stable API responses
For applications that use a business-code contract, register the optional plugin once:
import { ApiException, apiResponsePlugin, createRoute } from 'next-route-kit'
const ResponseCode = {
SUCCESS: { code: 'OK', msg: 'Success' },
QUOTA_EXCEEDED: { code: 'QUOTA_EXCEEDED', msg: 'Quota exceeded', status: 409 },
INTERNAL_ERROR: { code: 'INTERNAL_ERROR', msg: 'Internal server error' },
} as const
const apiRoute = createRoute({
plugins: [apiResponsePlugin({ success: ResponseCode.SUCCESS, systemError: ResponseCode.INTERNAL_ERROR })],
})
export const POST = apiRoute({
handler: async () => {
if (/* application rule */ false) {
throw new ApiException(ResponseCode.QUOTA_EXCEEDED)
}
return { resourceId: 'resource-demo' }
},
})Handler results and ApiException values are converted to one
{ code, msg, data } envelope. Codes may be strings or numbers, and data
preserves objects, arrays, primitives, and null. The code constants remain
application-owned, so a client can handle common auth/quota codes globally and
feature-specific codes locally. Native Response values pass through by default;
set nativeResponse: 'reject' on a strict JSON Factory. Unexpected errors use the configured system response and are
reported with console.error unless onUnknownError supplies an application
reporter.
Validation is not built into this response contract. The main package does not
depend on Zod or register a Zod filter. If an application installs the optional
@next-route-kit/zod adapter, use apiResponsePlugin({ mapError }) to map
ZodValidationError into the envelope. Use zodExceptionFilter() instead only
for a route that intentionally uses the adapter's standalone JSON shape.
The pipeline is:
Next params → Middleware → Guard → Interceptor enter
→ declared arguments → Pipe → Handler → Interceptor exit → ResponseErrors go through ExceptionFilter.catch(). The package supplies a default
filter for HttpError and malformed JSON, plus a default JSON serializer. A
native Response returned by a handler passes through unchanged unless the
Factory uses the strict response policy.
Automatic body resolvers enforce a 1 MiB default limit. Configure
maxBodyBytes on a dedicated Factory when a JSON domain needs a different
explicit ceiling. Use withLocals(provider) to derive required Handler locals
from values actually produced before body parsing.
See the root README for RESTful examples and the user guides for API details.
Custom plugins
Create a class that implements RoutePlugin and return reusable lifecycle
components from install():
import { createRoute, type RoutePlugin } from 'next-route-kit'
class RequestTimingPlugin implements RoutePlugin {
readonly name = 'request-timing'
readonly runtime = 'both' as const
install() {
return {
interceptors: [
{
name: 'request-timing',
async intercept(_context, next) {
const startedAt = Date.now()
try {
return await next()
} finally {
console.info('durationMs:', Date.now() - startedAt)
}
},
},
],
}
}
}
const route = createRoute({
plugins: [new RequestTimingPlugin()],
})Plugins can contribute middleware, guards, pipes, interceptors,
exceptionFilters, or one responseSerializer. Register them on the base Factory,
on extend() for a subgroup, or with route-local use: [plugin]. The detailed
plugin guide
documents the lifecycle order and scope rules.
