Safe route handler
API reference for the safe Route Handler factory — segments, search params, JSON body or form data, auth, and HTTP error responses.
createSafeRouteHandler wraps a Next.js Route Handler (route.ts / route.js) with optional validation of dynamic segments, search params, JSON body or form data, optional authorization, and centralized error handling. Schemas use Standard Schema; dictionaries use the package’s Standard Schema dictionary shape (see types TSegmentsDict, TSearchParamsDict, TFormDataDict).
Import
importfrom '@sugardarius/anzen'Signature
function createSafeRouteHandler<
AC = undefined,
TSegments = undefined,
TSearchParams = undefined,
TBody = undefined,
TFormData = undefined,
TReq extends Request = Request,
>(
options: CreateSafeRouteHandlerOptions<
AC,
TSegments,
TSearchParams,
TBody,
TFormData
handler:
ctx: SafeRouteHandlerContext<
AC,
TSegments,
TSearchParams,
TBody,
TFormData
req: TReq
=> Promise<Response>
):
req: TReq,
providedContext:params: Awaitable<any> | undefined
=> Promise<Response>The returned function matches what Next.js expects for GET, POST, etc. Use NextRequest as TReq by annotating the handler’s second parameter if you need nextUrl and friends; the cloned request passed to authorize is still a web Request (see below).
Types: CreateSafeRouteHandlerOptions, SafeRouteHandlerContext, RouteHandlerAuthFunction, etc.
Using NextRequest type
By default the factory uses the native Request type. If you want to use the NextRequest type from Next.js, you can do it by just using the NextRequest type in the factory handler.
importfrom 'next/server'
importfrom '@sugardarius/anzen'
export const GET = createSafeRouteHandler
'next/request',
authorize: async
// Due to `NextRequest` limitations as the req is cloned it's always a `Request`
=>
log
return'John Doe'
asyncctx, req: NextRequest) =>
log'pathname', req.nextUrl.pathname)
return new Responsenull, 200
Options
When creating a safe route handler you can use a bunch of options for helping you achieve different tasks 👇🏻
id
id?: string
Used for logging in development or when the debug option is enabled. You can also use it to add extra logging or monitoring.
By default the id is set to [unknown:route:handler].
export const POST = createSafeRouteHandler
'auth/login',
asyncid=>
returnjson
onErrorResponse
onErrorResponse?: (err: unknown) => Awaitable<Response>
Callback triggered when the request fails.
By default it returns a simple 500 response and the error is logged into the console.
Use it if your handler use custom errors and you want to manage them properly by returning a proper response.
You can read more about it under the Error handling section.
debug
debug?: boolean
Use this options to enable debug mode. It will add logs in the handler to help you debug the request.
By default it's set to false for production builds.
In development builds, it will be true if NODE_ENV is not set to production.
importfrom '@sugardarius/anzen'
export const GET = createSafeRouteHandlertrueasync=>
return new Responsenull, { status: 200
})authorize
authorize?: RouteHandlerAuthFunction<AC, TSegments, TSearchParams, TBody, TFormData>
Function to use to authorize the request. By default it always authorize the request.
Returns a response when the request is not authorized.
The authorize function receives validated props (segments, searchParams, body, formData) when they are defined, allowing you to use validated data for authorization logic.
Parameters:
id: string- Route handler IDurl: URL- Parsed request URLreq: Request- Cloned request (to avoid side effects and make it consumable)segments?:- Validated route dynamic segments (ifsegmentsoption is defined)searchParams?:- Validated search params (ifsearchParamsoption is defined)body?:- Validated request body (ifbodyoption is defined)formData?:- Validated form data (ifformDataoption is defined)
Basic authorization
importfrom '@sugardarius/anzen'
importfrom '~/lib/auth'
export const GET = createSafeRouteHandler
authorize: asyncreq, url=>
log'url', url)
const session = awaitgetSession
if!session) {
return new Responsenull, { status: 401
return
asyncauthreq): Promise<Response> =>
returnjson200
Authorization with validated segments
importfrom 'zod'
importfrom '@sugardarius/anzen'
importfrom '~/lib/auth'
export const GET = createSafeRouteHandler
string
string
authorize: asyncsegments, req=>
// segments are already validated at this point
const session = awaitgetSession
if!session) {
return new Responsenull, { status: 401
// Check if user has access to this account
const hasAccess = await checkAccountAccess
if!hasAccess) {
return new Responsenull, { status: 403
return
asyncauth, segments=>
returnjson200
Authorization with validated body
importfrom 'zod'
importfrom '@sugardarius/anzen'
export const POST = createSafeRouteHandler
object
string
authorize: asyncbody, req=>
// body is already validated at this point
const isValidKey = await validateApiKey
if!isValidKey) {
return new Responsenull, { status: 401
return
asyncauth, body=>
returnjson200
Authorization with all validated props
importfrom 'zod'
importfrom '@sugardarius/anzen'
importfrom '~/lib/auth'
export const POST = createSafeRouteHandler
string
string
objectstring
authorize: asyncsegments, searchParams, body, req=>
// All props are validated and available
const session = awaitgetSession
if!session) {
return new Responsenull, { status: 401
const hasPermission = await checkPermission
if!hasPermission) {
return new Responsenull, { status: 403
return
asyncauth, segments, searchParams, body=>
returnjson
200
The original request is cloned from the incoming request to avoid side effects and to make it consumable in the
authorizefunction. Due toNextRequestlimitations, the cloned request is always aRequesttype.
segments
segments?: TSegments
Dynamic route segments used for the route handler path. By design it will handle if the segments are a Promise or not.
Please note the expected input is a StandardSchemaDictionary.
importfrom 'zod'
importfrom '@sugardarius/anzen'
export const GET = createSafeRouteHandler
string
stringoptional
asyncsegments=>
returnjson
onSegmentsValidationErrorResponse
onSegmentsValidationErrorResponse?: OnValidationErrorResponse
Callback triggered when dynamic segments validations returned issues. By default it returns a simple 400 response and issues are logged into the console.
importfrom 'zod'
importfrom '@sugardarius/anzen'
export const GET = createSafeRouteHandler
string
stringoptional
onSegmentsValidationErrorResponse: (issues) =>
returnjson400
asyncsegments=>
returnjson
searchParams
searchParams?: TSearchParams
Search params used in the route.
Please note the expected input is a StandardSchemaDictionary.
importfrom 'decoders'
importfrom '@sugardarius/anzen'
export const GET = createSafeRouteHandler
optional
asyncsearchParams=>
returnjson
onSearchParamsValidationErrorResponse
onSearchParamsValidationErrorResponse?: OnValidationErrorResponse
Callback triggered when search params validations returned issues. By default it returns a simple 400 response and issues are logged into the console.
importfrom 'decoders'
importfrom '@sugardarius/anzen'
export const GET = createSafeRouteHandler
optional
onSearchParamsValidationErrorResponse: (issues) =>
returnjson400
asyncsearchParams=>
returnjson
body
body?: TBody
Request body.
Returns a 405 response if the request method is not POST, PUT or PATCH.
Returns a 415response if the request does not explicitly set the Content-Type to application/json.
Please note the body is parsed as JSON, so it must be a valid JSON object. Body shouldn't be used with formData at the same time. They are exclusive.
Why making the distinction? formData is used as a StandardSchemaDictionary whereas body is used as a StandardSchemaV1.
importfrom 'zod'
importfrom '@sugardarius/anzen'
export const POST = createSafeRouteHandler
object
string
string
string
asyncbody=>
returnjson
When validating the body the request is cloned to let you consume the body in the original request (e.g second arguments of handler function).
onBodyValidationErrorResponse
onBodyValidationErrorResponse?: OnValidationErrorResponse
Callback triggered when body validation returned issues. By default it returns a simple 400 response and issues are logged into the console.
importfrom 'zod'
importfrom '@sugardarius/anzen'
export const POST = createSafeRouteHandler
object
string
string
string
onBodyValidationErrorResponse: (issues) =>
returnjson400
asyncbody=>
returnjson
formData
formData?: TFormData
Request form data.
Returns a 405 response if the request method is not POST, PUT or PATCH.
Returns a 415response if the request does not explicitly set the Content-Type to multipart/form-data or to application/x-www-form-urlencoded.
Please note formData shouldn't be used with body at the same time. They are exclusive.
Why making the distinction? formData is used as a StandardSchemaDictionary whereas body is used as a StandardSchemaV1.
importfrom 'zod'
importfrom '@sugardarius/anzen'
export const POST = createSafeRouteHandler
string
string
asyncformData=>
returnjson
When validating the form data the request is cloned to let you consume the form data in the original request (e.g second arguments of handler function).
onFormDataValidationErrorResponse
onFormDataValidationErrorResponse?: OnValidationErrorResponse
Callback triggered when form data validation returned issues. By default it returns a simple 400 response and issues are logged into the console.
importfrom 'zod'
importfrom '@sugardarius/anzen'
export const POST = createSafeRouteHandler
string
string
onFormDataValidationErrorResponse: (issues) =>
returnjson400
asyncformData=>
returnjson
Handler context
SafeRouteHandlerContext is the context object that is passed to the handler.
| Field | When |
|---|---|
id | Always. |
url | Parsed request URL. |
auth | When authorize returned an object. |
segments | When segments option is set and validation succeeded. |
searchParams | When searchParams option is set. |
body | When body option is set. |
formData | When formData option is set. |
The handler’s second argument is the original req (use NextRequest in the signature if needed).
Error handling
By design the factory will catch any error thrown in the route handler will return a simple response with 500 status.
You can customize the error response if you want to fine tune error response management.
importfrom '@sugardarius/anzen'
importfrom '~/lib/errors'
importfrom '~/lib/db'
export const GET = createSafeRouteHandler
onErrorResponse: asyncerr: unknown): Promise<Response> =>
ifinstanceof HttpError
return new Response
else ifinstanceof DbUnknownError
return new Response
return new Response'Internal server error', { status: 500
async: Promise<Response> =>
constdata, err] = awaitfindUnique'liveblocks'
if
throw new DbUnknownError500
if=== null
throw new HttpError404
returnjson
Validation failures use the on*ValidationErrorResponse hooks or the defaults described in Options (400).
Synchronous Validation
Validation must be synchronous. The Standard Schema contract discourages async validation. If a schema resolves validation asynchronously, behavior is undefined and may throw at runtime.
Fair use note
Please note that if you're not using any of the proposed options in createSafeRouteHandler it means you're surely don't need it.
// Calling 👇🏻
export const GET = createSafeRouteHandlerasync=>
return new Responsenull, { status: 200
})
// is equal to declare the route handler this way 👇🏻
export function GET() {
return new Responsenull, { status: 200
}
// excepts `createSafeRouteHandler` will provide by default a native error catching
// and will return a `500` response. That's the only advantage.See also
Last updated on