Build restful API methods for Next.js > 9 and validate the incoming requests with yup.
Next.js brought API routes support in v9, but you have to provide your own implementation of handling different rest methods (GET, POST, PUT, PATCH, DELETE). This helper enables you to clearly structure your method handling and validation.
- Install with
npm i @appgeist/restful-next-apioryarn add @appgeist/restful-next-api; - Run
npx install-peerdeps -do @appgeist/restful-next-apito make sure you have the necessarypeerDependencies(yupand of coursenext) in your project.
In /pages/api/products.js:
import { object, number, string } from 'yup';
import methods from '@appgeist/restful-next-api';
import { Product, User } from '~/models';
import { log } from '~/utils';
export default methods({
get: ({ query: { page } }) => Product.browse({ page }),
post: {
bodySchema: object({
name: string()
.min(5)
.max(20)
.required(),
description: string()
.min(5)
.max(1000),
price: number()
.positive()
.max(9999)
.required(),
inventoryItems: number()
.integer()
.positive()
.max(999)
.required()
}).noUnknown(),
onRequest: async ({ body, req }) => {
const product = await Product.create(body);
await log(`Product ${product.id} created at ${new Date()} by user ${req.userId}`);
return product;
}
}
});In /pages/api/products/[id].js:
import { object, number, string } from 'yup';
import { FORBIDDEN } from 'http-status-codes';
import methods, { ApiError } from '@appgeist/restful-next-api';
import { Product } from '~/models';
import { log } from '~/utilities';
export default methods({
patch: {
querySchema: {
id: number()
.integer()
.positive()
.required()
},
bodySchema: object({
name: string()
.min(5)
.max(20)
.required(),
description: string()
.min(5)
.max(1000),
price: number()
.positive()
.max(9999)
.required(),
inventoryItems: number()
.integer()
.positive()
.max(999)
.required()
}).noUnknown(),
onRequest: async ({ body, req }) => {
const product = await Product.create(body);
await log(`Product ${product.id} updated at ${new Date()} by user ${req.userId}`);
return product;
}
},
delete: {
querySchema: {
id: number()
.integer()
.positive()
.required()
},
onRequest: async ({ query: { id }, req }) => {
const { userId } = req;
const acl = await User.getACL(userId);
if (!acl.includes('deleteProduct')) throw new ApiError(FORBIDDEN);
await Product.destroy(id);
await log(`Product ${id} deleted at ${new Date()} by user ${userId}`);
}
}
});Each method can be:
- a request handler function (see details below)
- an object shaped like so:
{ querySchema, bodySchema, handler, errorHandler }.
A querySchema/bodySchema definition can be:
- a simple JS object for brevity (the object will be converted automatically to a yup schema)
- a yup schema (for complex scenarios when you need to add a
.noUnknown()modifier)
-
For each request, the
beforeRequesthandler is invoked if present:import methods from '@appgeist/restful-next-api'; export default methods({ get: { beforeRequest: () => { console.log('Before GET'); }, onRequest: () => { console.log('On GET request'); } }, delete: () => { console.log('On DELETE request'); }, beforeRequest: () => { // ... console.log('Before REQUEST'); // ... } });
-
If
beforeRequestcompletes without throwing an error, the data for each request is validated (and transformed) according to the specifiedquerySchemaandbodySchemadefinitions. Seeyupreadme for more information on data validation and transformation.- If validation fails, the request handler invocation is skipped and a
400(BAD_REQUEST) response is sent to the client with aJSONbody type structured like so:
{ "message": "There were 2 validation errors", "errors": ["body.price must be an integer", "body.inventoryItems is required"] }- If validation succeeds, the
onRequesthandler will be invoked.
- If validation fails, the request handler invocation is skipped and a
-
The
onRequesthandler:function onRequest({ query, body, req }) => { /* do work and return data */ };
...or
async function onRequest({ query, body, req }) => { /* do work and return Promise which resolves to data */ };
This method can return an object or a Promise resolving to an object that will be serialized to
JSONand sent back to the client with a200(OK) status code. IfonRequestreturnsundefinedornull, an empty response will be sent with a201(CREATED) header forPOSTrequests and204(NO_CONTENT) for non-POSTrequest. -
Default error handling
If
beforeRequestoronRequestthrows anApiError(also exported by@appgeist/restful-next-api), a specific http status code is returned to the client. For instance, the following code will result in a403(FORBIDDEN) being sent to the client:import methods, { ApiError } from '@appgeist/restful-next-api'; import { FORBIDDEN } from 'http-status-codes'; export default methods({ get: { // ... onRequest: () => { // ... throw new ApiError(FORBIDDEN); // ... } // ... } });
Other error types are treated as
500/INTERNAL_SERVER_ERRORand are also logged to the console.
You can override the default error handling mechanism by providing a custom error handling function like so:
export default methods({
patch: {
// querySchema: ..., bodySchema: ...,
onRequest: ({ body, req }) => {
/* handle patch request */
},
// Error handler for patch requests
onError: ({ res, err }) => {
res.status(500).send('Error while trying to patch');
}
},
delete: {
// querySchema: ...,
onRequest: ({ query: { id }, req }) => {
/* handle delete request */
}
},
// Generic error handler - this will also handle errors for delete requests
onError: ({ res, err }) => {
res.status(500).send('Error');
}
});A specific method error handler takes precedence over the generic error handler.
JsDocs are provided for IDE support for now; an index.d.ts will be provided at some point in the future.
The ISC License.
