@ohm-vision/next-apiroute
v1.0.2
Published
Wrapper for NextJS App Router API routes
Downloads
9
Maintainers
Readme
next-apiroute
Wrapper for NextJS App Router API routes
This wrapper will perform standard basic validation and protection steps for your API routes to standardize your control flow
Note: This library works only for Next API's using the App Router (v13+)
Installation
Run the following command
npm install @ohm-vision/next-apiroute
Usage
Simply import the ApiRoute
into your route.ts
file and assign it to the standard GET
, POST
, PUT
, PATCH
or DELETE
operations supported by NextJS
The library uses yup
to validate the request objects before they hit your handler
Important notes
- Each step checks if the request received an aborted signal and will shortcircuit with a
418_IM_A_TEAPOT
http status code - If a
HttpError
type error is thrown (any object with astatusCode
property), that value will be returned to the client - If the request is aborted by the client, a
418_IM_A_TEAPOT
status is returned - If a
ValidationError
object is thrown, a400_BAD_REQUEST
status is returned along with the error in the body - All other errors, result in a
500_INTERNAL_ERROR
status code and the details are logged
Order of execution
readSession
- If
validate.sessionSchema
is provided, validate the session object, logs and sets session tonull
if it fails
- If
- If
secure
is true, validate that the session is notnull
or return401_UNAUTHORIZED
- If
validate.paramsSchema
is provided, validate the requestparams
object, if fails, returns a400_BAD_REQUEST
along with the accompanyingyup
ValidationError
object in the body - If
validate.searchSchema
is provided, validate theNextRequest.nextUrl.searchParams
object, if fails, returns a400_BAD_REQUEST
along with the accompanyingyup
ValidationError
object in body readBody
- If
validate.bodySchema
is provided, validate the request body, if fails, returns a400_BAD_REQUEST
along with the accompanyingyup
ValidationError
object in the body
- If
isAuthorized
, if this returns false a403_FORBIDDEN
response is returnedisValid
, if this returns false a400_BAD_REQUEST
response is returnedhandler
, fires business logic- If the result is a
NextResponse
orResponse
object, it is passed back to the client (use this when passing backBlob
,redirects
, files or other special types of responses) - If the result is null, an empty
200_OK
response is returned - If the result is a string, the string is returned in the body along with a
200_OK
response - Otherwise:
- The response object is converted to a
NextJS
-compatible object viaJSON.parse(JSON.stringify(obj))
, - The response object is recursively stripped of all properties starting with an underscore (
_
) - A
200_OK
status code is returned along with the mutated response body
- The response object is converted to a
- If the result is a
Props
The props below outline how you can configure each request
- name (string): gives a name to the API when logging
- secure (boolean): returns an
401_UNAUTHORIZED
http status code to the client if no session is found (defaults to true) - log (ILogger): A logger interface for capturing messages in flight
- validate (object)
- sessionSchema: A
yup
object schema used to validate the session
- sessionSchema: A
- paramsSchema: A
yup
object schema used to validate the path params
- paramsSchema: A
- searchSchema: A
yup
object schema used to validate the querystring
- searchSchema: A
- bodySchema: A
yup
object schema used to validate the request body
- bodySchema: A
- readBody(req:
NextRequest
): The async function to read the request body, usually this will bereq => req.json()
- readSession(req:
NextRequest
): The async function to decode and resolve the current user session - isAuthorized(props:
DecodedProps
): Additional async authorization functions, maybe to verify the user is allowed to access the resource, returningfalse
here will return aFORBIDDEN
http status code - isValid(props:
DecodedProps
): Additional async validation functions, maybe to check if the resource is valid - returningfalse
here will return in a400_BAD_REQUEST
http status code - handler(props:
DecodedProps
): Async function for the actual business logic
Example
import { ApiRoute } from "@ohm-vision/next-apiroute";
import { mixed, number, object, string, InferType, date, array } from "yup";
// @/models/sessions/session.model.ts
const permissions = [
"read", "write"
] as const;
type Permission = typeof permissions[number];
const sessionSchema = object({
userName: string().required().nullable(),
expiry: date().max(new Date()).required().nullable(),
permissions: array(mixed<Permission>().oneOf(permissions))
});
type Session = InferType<typeof sessionSchema>;
// @/models/blogs/search-blogs.dto.ts
const searchBlogsDtoSchema = object({
search: string().nullable(),
limit: number().min(1).max(10).nullable(),
});
// @/app/api/blogs/[type]/route.ts
const types = [
"red", "orange", "yellow"
] as const;
type TypeParam = typeof types[number];
const name = "MyApi";
export const GET = ApiRoute({
name: name,
secure: false,
log: console,
readSession: async (req) => {
// todo: plug into session resolver
const session: Session = null;
return session;
},
validate: {
paramsSchema: object({
type: mixed<TypeParam>().oneOf(types).required()
}) ,
searchSchema: searchBlogsDtoSchema,
sessionSchema: sessionSchema,
},
handler: async ({ params: { type }, searchParams: { search, limit }}) => {
const result = [];
// todo: look up the blogs
return result;
}
});
export const POST = ApiRoute({
name: name,
secure: true,
log: console,
readSession: async (req) => {
// todo: plug into session resolver
const session: Session = null;
return session;
},
readBody: req => req.json(),
validate: {
bodySchema: object({
title: string().required().nullable(),
}),
paramsSchema: object({
type: mixed<TypeParam>().oneOf(types).required()
}),
sessionSchema: sessionSchema,
},
// additional permission checking
isAuthorized: async ({ session }) => session.permissions.includes("write"),
// additional validation
isValid: async ({ body, params }) => {
// todo: additional validation
return true;
},
handler: async ({
body: { title }, params: { type }
}) => {
// todo: save to db
return "works";
}
});