1.0.2 • Published 1 year ago
@ohm-vision/next-apiroute v1.0.2
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-apirouteUsage
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_TEAPOThttp status code - If a
HttpErrortype error is thrown (any object with astatusCodeproperty), that value will be returned to the client - If the request is aborted by the client, a
418_IM_A_TEAPOTstatus is returned - If a
ValidationErrorobject is thrown, a400_BAD_REQUESTstatus is returned along with the error in the body - All other errors, result in a
500_INTERNAL_ERRORstatus code and the details are logged
Order of execution
readSession- If
validate.sessionSchemais provided, validate the session object, logs and sets session tonullif it fails
- If
- If
secureis true, validate that the session is notnullor return401_UNAUTHORIZED - If
validate.paramsSchemais provided, validate the requestparamsobject, if fails, returns a400_BAD_REQUESTalong with the accompanyingyupValidationErrorobject in the body - If
validate.searchSchemais provided, validate theNextRequest.nextUrl.searchParamsobject, if fails, returns a400_BAD_REQUESTalong with the accompanyingyupValidationErrorobject in body readBody- If
validate.bodySchemais provided, validate the request body, if fails, returns a400_BAD_REQUESTalong with the accompanyingyupValidationErrorobject in the body
- If
isAuthorized, if this returns false a403_FORBIDDENresponse is returnedisValid, if this returns false a400_BAD_REQUESTresponse is returnedhandler, fires business logic- If the result is a
NextResponseorResponseobject, 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_OKresponse is returned - If the result is a string, the string is returned in the body along with a
200_OKresponse - 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_OKstatus 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_UNAUTHORIZEDhttp 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
yupobject schema used to validate the session
- sessionSchema: A
- paramsSchema: A
yupobject schema used to validate the path params
- paramsSchema: A
- searchSchema: A
yupobject schema used to validate the querystring
- searchSchema: A
- bodySchema: A
yupobject 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, returningfalsehere will return aFORBIDDENhttp status code - isValid(props:
DecodedProps): Additional async validation functions, maybe to check if the resource is valid - returningfalsehere will return in a400_BAD_REQUESThttp 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";
}
});