@moxy/next-rest-api
v2.1.0
Published
Aims to ease the development of REST APIs in Next.js
Downloads
16
Readme
next-rest-api
Aims to ease the development of REST APIs in Next.js.
Motivation
Next.js brought API routes support in v9, but you have to provide your own implementation for handling different HTTP methods, validation, error handling and so on. So in short, this library provides a standard way to:
- Detect HTTP methods (GET, POST, PUT, PATCH, DELETE, etc).
- Validate the request (headers, query, body).
- Handle errors, including how their responses look like.
- Log errors, by printing them to
stderr
by default.
Installation
$ npm install @moxy/next-rest-api joi @hapi/boom
This library has a peer-dependency on joi
and @hapi/boom
to perform validation and to output errors in a standard format.
Usage
Simple get endpoint:
In /pages/api/products.js
(or /pages/api/products/index.js
)
import withRest from '@moxy/next-rest-api';
export default withRest({
GET: async (req, res) => {
const products = await listProducts();
// You may do some post-processing of `products` here...
return products;
},
});
Simple get & post endpoint, with validation:
In /pages/api/products.js
(or /pages/api/products/index.js
)
import withRest, { withValidation } from '@moxy/next-rest-api';
import Joi from 'joi';
import Boom from '@hapi/boom';
const getSchema = {
query: Joi.object({
q: Joi.string(),
sortBy: Joi.valid('price:asc', 'price:desc'),
}),
};
const postSchema = {
body: Joi.object({
name: Joi.string().max(200).required(),
description: Joi.string().max(2000),
price: Joi.number().min(0).required(),
}),
};
export default withRest({
GET: withValidation(getSchema)(async (req, res) => {
const products = await listProducts(req.query);
return products;
}),
POST: withValidation(postSchema)(async (req, res) => {
const product = await createProduct(req.body);
return product;
}),
});
ℹ️ You may use p-compose
to compose your "middlewares" to be more readable, like so:
import withRest, { withValidation } from '@moxy/next-rest-api';
import compose from 'p-compose';
export default withRest({
GET: compose(
withValidation(getSchema),
async (req, res) => {
const products = await listProducts(req.query);
return products;
},
),
});
Simple get, put and delete endpoints, with validation:
In /pages/api/products/[id].js
ℹ️ In Next.js, dynamic parameters are assigned to the request query (req.query.id
in this case).
import withRest, { withValidation } from '@moxy/next-rest-api';
import Joi from 'joi';
import Boom from '@hapi/boom';
const getSchema = {
query: Joi.object({
id: Joi.string().required(),
}),
};
const putSchema = {
query: getSchema.query,
body: Joi.object({
name: Joi.string().max(200).required(),
description: Joi.string().max(2000),
price: Joi.number().min(0).required(),
}),
};
const deleteSchema = {
query: getSchema.query,
};
export default withRest({
GET: withValidation(getSchema)(async (req, res) => {
let product;
try {
product = await getProduct(req.query.id);
} catch (err) {
if (err.code === 'NOT_FOUND') {
throw Boom.notFound(`Product with id ${req.query.id} does not exist`);
}
throw err;
}
return product;
}),
PUT: withValidation(putSchema)(async (req, res) => {
let product;
try {
product = await updateProduct(req.query.id, req.body);
} catch (err) {
if (err.code === 'NOT_FOUND') {
throw Boom.notFound(`Product with id ${req.query.id} does not exist`);
}
throw err;
}
return product;
}),
DELETE: withValidation(deleteSchema)(async (req, res) => {
try {
product = await deleteProduct(req.query.id);
} catch (err) {
if (err.code === 'NOT_FOUND') {
return;
}
throw err;
}
},
});
ℹ️ A lot of schemas in the above examples are being repeated. To keep things DRY, it's advisable to reuse them, perhaps in a schemas.js
file.
API
withRest(methods, [options])
Matches handlers defined in methods
against the HTTP method, like GET
or POST
.
Handlers may return any valid JSON as per the RFC7159, which includes objects, arrays, booleans and null (undefined is coerced to null). The return value will be sent automatically as a JSON response.
Exceptions thrown within handlers will be caught automatically and sent to the client. You may either throw a Boom error or a standard error object. If a standard error object is thrown, it will be converted to a Boom error instance automatically (500
).
In case you throw a Boom error, you may optionally pass the original error inside data.originalError
, making the default error logger print that error instead of the Boom wrapper (when within the 5xx
range). In fact, this is already done automatically for you when you throw a standard error object inside handlers.
Here's an example on how to pass the original error:
try {
await deleteProduct(req.query.id);
} catch (err) {
throw Boom.internal('Unable to delete product', { originalError: err });
}
methods
Type: object
An object mapping HTTP methods to their handlers with the following signature: async (req, res) => {}
.
options
Type: object
sendError
Type: function
Default: see defaultSendError
in index.js
A function responsible to send Boom errors back to the client. Has the following signature: (res, err) => {}
.
The default implementation uses the output
property of the Boom error to set the response headers, status code and payload.
logError
Type: function
Default: see defaultLogError
in index.js
A function that logs errors. Has the following signature: (err) => {}
.
The default implementation ignores any non 5xx
and simply prints the error stack to stderr
. If the error contains data.originalError
, that error's stack is printed instead.
withValidation(schemas)
Wraps a handler with validation against Joi schemas.
If validation fails, a 400 Bad Request
response will be sent back to the client.
schemas
Type: object
An object with query
, body
or headers
keys and their associated Joi schemas. Each of these schemas will be matched against the incoming request.
⚠️ Generally you only want to validate a subset of headers. In such situations, your headers
schema should allow unknown keys with .unknown(true)
.
How to test your API
To create unit tests for your API, we recommend using supertest
. However, we have to inject Next.js middleware and helpers into req
and res
ourselves.
Given the following API endpoint:
// pages/api/hello.js
import withRest from '@moxy/next-rest-api';
export default withRest()({
get: () => 'hello',
});
Here's how you could test it:
// pages/api/hello.test.js
import request from 'supertest';
import { apiResolver } from 'next/dist/next-server/server/api-utils';
import hello from './hello';
const enhance = (handler) => (req, res) => apiResolver(req, res, undefined, handler);
it('should print hello', async () => {
await request(enhance(hello))
.get('/')
.expect(200)
.then((res) => {
expect(res.body).toBe('hello');
});
});
⚠️ Note: If you want supertest
to return a promise, you must use .then()
as mentioned in the example above. Otherwise, it will not return a promise and hence, you can't await
it.
Tests
$ npm t
$ npm t -- --watch # To run watch mode
License
Released under the MIT License.