It's like the missing API endpoint layer for Next.js
type ResponseData = {
result: string
over: number
}
export const GET = handler<ResponseData>(async (req) => {
return NextResponse.json({
result: "this response is type-checked",
over: 9000
})
})
Note
This library is designed for Next.js 15 and higher. To use this library with Next.js 14 or earlier, use typed-route-handler version 0.3.0
.
- ✅ Type-safe route handler responses
- ✅ Type-safe route handler parameters
- ✅ Extended Next.js error handling
- ✅ Full zod compatibility
- ✅ Route handler timing
- ✅ Request logging
- ✅ Production ready
npm i typed-route-handler zod next
Typed handler is easy to use: In the simplest case, just wrap your Route Handler with handler
and you're good to go!
+ import { handler } from 'typed-route-handler'
- export const GET = async (req: NextRequest) => {
+ export const GET = handler(async (req) => {
// ...
- }
+ })
The real magic comes when you add typing to your responses.
import { NextResponse } from "next"
type ResponseData = {
name: string
age: number
}
export const GET = handler<ResponseData>((req) => {
// ...
return NextResponse.json({
name: "Bluey",
age: 7,
something: "else" // <-- this will cause a type error
})
})
We can also add type verification to our parameters. Each parameter Context
extends from NextRouteContext
which is a helper type mapping to: { params?: Record<string, string | string[]> }
.
import { NextResponse } from "next"
type ResponseData = {
name: string
}
type Context = {
params: {
userId: string
}
}
export const GET = handler<ResponseData, Context>((req, context) => {
// ...
const userId = context.params.userId // <-- this will be type-safe
return NextResponse.json({
name: "Bluey"
})
})
This can get even more powerful with zod
import { NextResponse } from "next"
import { z } from "zod"
type ResponseData = {
name: string
}
const contextSchema = z.object({
params: z.object({
id: z.string()
})
})
export const GET = handler<ResponseData, z.infer<typeof contextSchema>>(
(req, context) => {
// ...
const userId = context.params.userId // <-- this will still be type-safe
// or you can parse the schema:
const { params } = contextSchema.parse(context)
return NextResponse.json({
name: "Bluey"
})
}
)
Similarly, you can use zod
to parse request bodies:
import { NextResponse } from "next"
import { z } from "zod"
type ResponseData = {
name: string
}
const bodySchema = z.object({
username: z.string()
})
export const PUT = handler<ResponseData>((req, context) => {
const body = bodySchema.parse(await req.json())
// If the body does not satisfy `bodySchema`, the route handler will catch
// the error and return a 400 error with the error details.
return NextResponse.json({
name: body.username
})
})
When a zod error is thrown in the handler, it will be caught automatically and converted to a Validation Error with a 400 status code.
Example:
{
"error": "Validation Error",
"issues": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": ["name"],
"message": "Required"
}
]
}
This library adds the following convenience methods to Route Handlers.
Similar to how Next.js offers notFound()
and redirect()
, typed-route-handler offers:
unauthorized()
forbidden()
validationError()
For example:
export const GET = handler(async (req) => {
const session = await auth()
if (!session) {
unauthorized()
}
})
This will return the following HTTP 401 Unauthorized body:
{
"error": "Unauthorized"
}
When using this library with next-auth or other libraries which modify the req
objects, you can pass a 3rd type to the handler
call. You may also need to place handler
within the other middleware because the other handlers may mask the return types, disabling the type-checking from typed-route-handler
For example:
import { auth } from '@/auth'
import { type NextAuthRequest } from 'next-auth'
import { handler, type type NextRouteContext, unauthorized } from 'typed-route-handler'
export const GET = auth(
handler<ResponseBody, NextRouteContext, NextAuthRequest>((req, ctx) => {
if (!req.auth?.user) {
unauthorized()
}
// ...
})
)
By default all errors are handled by the handler. However, it is often smart to send an issue to a bug reporting tool like Sentry. To dot his, you can pass a second argument to handler
which is an onError callback.
import { handler } from "typed-route-handler"
export const GET = handler(
() => {
throw new Error()
},
(err) => {
console.log("onError callback!")
Sentry.captureException(err)
}
)
typed-route-handler
comes with a client library that extends the traditional fetch
API with type information.
The typedFetch
function will automatically parse the response as JSON, and apply the proper types. On an error response, it will throw.
import { typedFetch } from "typed-route-handler/client"
const data = await typedFetch<{ id: number; username: string }>("/api/user")
data.id // <-- number
data.username // <-- string
If there's an API error, it will be thrown by the client:
import { typedFetch } from "typed-route-handler/client"
try {
await typedFetch("/api/user")
} catch (e) {
e.message // <-- Validation Error, etc
}
- Add support for streaming responses (generic
Response
type) - Add support for custom API response formats
- Client-side error handling with zod issues
Already widely used in high-traffic production apps in songbpm, jog.fm, usdc.cool, as well as all StartKit projects.
This project is MIT-licensed and is free to use and modify for your own projects.
It was created by Matt Venables.