An opinionated approach to achieving type safety in your Next.js project without sacrificing developer experience or the simplicity of REST APIs
Built with zero dependencies and a small API surface
Pages or app router, it matters not. Both are supported
Linter errors when building and red squiggly lines in your editor
Run-time validation and sanitization of input and output data
Make requests to typed endpoints with confidence
Leverage the power of Zod to define your input and output schemas
INSTALLATION
npm install typed-handlers
The package exposes a next.js plugin that is used to generate the
route definitions. You will need to add the following to your
next.config.js
file. If you are using this with the pages
router, you will need to set the legacy
option to true
// next.config.js
import { withTypedHandlers } from 'typed-handlers/next';
export default withTypedHandlers({
//... next config
},{
legacy: true // pages router
});
Once you've added this to your config, you'll notice that a new
file typed-handlers.d.ts
is created add added to the include
options in your tsconfig.json
file. If this doesn't happen
automatically for whatever reason, ensure your setup resembles the following
// typed-handlers.d.ts
/// <reference types='typed-handlers/routes' />
// tsconfig.json
{
// ...
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"typed-handlers.d.ts"
],
}
And that's it! In dev mode, the package will watch for changes/new api routes and generate the necessary types for you to use in your app. In production, the package will generate the types at build time.
// app/api/hello.ts
import { NextResponse } from "next/server";
import { createHandler, schema } from "typed-handlers";
import { z } from "zod";
export const POST = createHandler(
async ({ body }) => {
return NextResponse.json({
message: `Hello, ${body.name}!`,
});
},
{
schema: schema({
body: z.object({
name: z.string({ required_error: "Name is required" }),
}),
}),
}
);
The example above demonstrates a simple POST request handler. It's similar to the default Next.js API route handler, but with a few key differences:
createHandler
utility
function from the typed-handlers
package. The second argument
is an options object that contains a
schema
property which is used to define the input and output
schemas for validation and type safety.
NextResponse
object instead of the usual Response
object. NextResponse
is generic while Response
is not.
request
, context
, body
, query
, and params
which have all been validated
(where applicable) and typed correctly.
request
and context
come from the Next.js API
route handler while body
, query
, and params
are the parsed request body, query string, and route parameters respectively
(using zod objects provided in the schema).
We can expand on this example to make it more full-featured as follows
// app/api/hello.ts
import { NextResponse } from "next/server";
import { createHandler, schema } from "typed-handlers";
import { z } from "zod";
export const POST = createHandler(
async ({ body, query }) => {
return NextResponse.json({
// output will be validated
message: `Hello, ${body.name}! You've requested page ${query.page}`,
});
},
{
schema: schema({
body: z.object({
name: z.string(),
}),
output: z.object({
message: z.string(),
}),
query: z.object({
page: z.coerce.number().optional().default(1),
}),
}),
onValidationError: ({ source, error }) => {
console.error(source); // -> 'body' | 'query'
console.error(error); // -> 'tis a ZodError instance
return NextResponse.json(
{ error: `Invalid request ${source}`, issues: error.issues },
{ status: 422 }
);
},
}
);
It's important to note that the package automatically handles any
validation errors and returns a 400
error code with an error
title and description. The onValidationError
option can be
used to customize this behavior.
While type-safety to input and output validation utilize the zod
schemas provided, type-safety for dynamic routes (therefore, params)
is a bit different. It happens via route inference i.e The package will infer
the types of the route parameters from the route itself. And since the
routes are typed, you can be sure that the types are correct.