Next.js is one of the leading React-based frameworks out there. With almost no learning curve on top of React.js and the fact that it provides great SEO out of the box, it has become a popular choice for many developers.
It uses file-system-based routing and also provides a flexible way to create API routes which is a great way to create serverless functions in Next.js with simplicity.
But, with simplicity and flexibility come the following issues:
- Redundant code for request method verification and validation.
- The official documentation suggests using lengthy if-else-if chains to handle multiple request methods.
- No conventions for handling API errors. This is a problem because we want to handle errors in a consistent way (so that we can catch them on the frontend) and also provide a way to log unexpected errors.
So to address these issues, I came up with a Higher Order Function (HOF) that abstracts away all the redundant code and error-handling so you can focus on your core business logic. All you need to worry about is when to return a response and when to throw an error. The error response will automatically be created by the Global Error Handler (Central Error Handling).
CREDITS: This method was inpired from https://jasonwatmore.com/post/2021/08/23/next-js-api-global-error-handler-example-tutorial
So without ado, let's dive in.
Chapters
- Project Setup
- API Handler Higher Order Function
- Global Error Handler
- Creating example API route
- Wrapping up
- Source Code
SIDENOTE: This is my first post on DEV so suggestions are not just welcome, they are mandatory! :P
1. Project Setup
Start by creating a Next.js project using the create-next-app
npm script. Notice that I'm using the --ts
flag to initialize with TypeScript because it's 2022 folks, make the switch!
npx create-next-app@latest --ts
# or
yarn create next-app --ts
We'll need to install two additional packages; one for schema validation (I prefer yup
) and another for declaratively throwing errors, http-errors
.
npm i yup http-errors && npm i --dev @types/http-errors
# or
yarn add yup http-errors && yarn add --dev @types/http-errors
2. API Handler Higher Order Function
Create a new file ./utils/api.ts
. This file exports the apiHandler()
function which acts as the entry point for any API route. It accepts a JSON mapping of common HTTP request methods and methodHandler()
functions. It returns an async function that wraps all the API logic into a try-catch
block which passes all the errors to errorHandler()
. More on that later.
// ./utils/api.ts
import createHttpError from "http-errors";
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { Method } from "axios";
// Shape of the response when an error is thrown
interface ErrorResponse {
error: {
message: string;
err?: any; // Sent for unhandled errors reulting in 500
};
status?: number; // Sent for unhandled errors reulting in 500
}
type ApiMethodHandlers = {
[key in Uppercase<Method>]?: NextApiHandler;
};
export function apiHandler(handler: ApiMethodHandlers) {
return async (req: NextApiRequest, res: NextApiResponse<ErrorResponse>) => {
try {
const method = req.method
? (req.method.toUpperCase() as keyof ApiMethodHandlers)
: undefined;
// check if handler supports current HTTP method
if (!method)
throw new createHttpError.MethodNotAllowed(
`No method specified on path ${req.url}!`
);
const methodHandler = handler[method];
if (!methodHandler)
throw new createHttpError.MethodNotAllowed(
`Method ${req.method} Not Allowed on path ${req.url}!`
);
// call method handler
await methodHandler(req, res);
} catch (err) {
// global error handler
errorHandler(err, res);
}
};
}
If a request is received for an unsupported request.method
, we throw a 405 (Method Not Allowed) Error. Notice how we're throwing the error using http-errors
. This is how we'll handle any expected errors in our business logic. It enforces a convention that all devs must follow on the project which produces consistent error responses.
NOTE: Inside ApiMethodHandlers
, I'm using the Method
type from axios
. If you don't want to install axios
, you can define it somewhere as,
export type Method =
|'GET'
|'DELETE'
|'HEAD'
|'OPTIONS'
|'POST'
|'PUT'
|'PATCH'
|'PURGE'
|'LINK'
|'UNLINK';
3. Global Error Handler
When an error is thrown anywhere in our APIs, it'll be caught by the top level try-catch
block defined in our apiHandler()
(unless ofcourse we define another error boundary somewhere deeper). The errorHandler()
checks for the type of error and responds accordingly.
// ./utils/api.ts
import { ValidationError } from "yup";
function errorHandler(err: unknown, res: NextApiResponse<ErrorResponse>) {
// Errors with statusCode >= 500 are should not be exposed
if (createHttpError.isHttpError(err) && err.expose) {
// Handle all errors thrown by http-errors module
return res.status(err.statusCode).json({ error: { message: err.message } });
} else if (err instanceof ValidationError) {
// Handle yup validation errors
return res.status(400).json({ error: { message: err.errors.join(", ") } });
} else {
// default to 500 server error
console.error(err);
return res.status(500).json({
error: { message: "Internal Server Error", err: err },
status: createHttpError.isHttpError(err) ? err.statusCode : 500,
});
}
}
All unforeseen errors are considered unhandled and are presented as 500 Internal Server Error
s to the users. I also chose to send the stack trace and log it in this case for debugging.
4. Creating example API route
With that, our apiHanlder()
is complete and can now be used instead of a plain old function export inside all API route files.
Let's create a demo route to see it in action.
I'll show this by creating a fake blog REST api /api/article?{id: string}
. Create a file under ./pages/api/
and name it article.ts
.
// ./pages/api/article.ts
import createHttpError from "http-errors";
import * as Yup from "yup";
import { NextApiHandler } from "next";
import { apiHandler } from "utils/api";
import { validateRequest } from "utils/yup";
// Fake DB to demonstrate the API
const BLOG_DB = [
{
id: 1,
title: "Top 10 anime betrayals",
content: "Lorem ipsum dolor sit amet ....",
publishedTimestamp: 1665821111000,
},
];
type GetResponse = {
data: typeof BLOG_DB | typeof BLOG_DB[number];
};
/**
* returns all articles or the article with the given id if query string is provided
*/
const getArticle: NextApiHandler<GetResponse> = async (req, res) => {
const { id } = req.query;
if (id) {
// find and return article with given id
const article = BLOG_DB.find((article) => article.id === Number(id));
if (!article)
throw new createHttpError.NotFound(`Article with id ${id} not found!`);
// OR
// if (!article) throw new createHttpError[404](`Article with id ${id} not found!`)
res.status(200).json({ data: article });
} else {
res.status(200).json({ data: BLOG_DB });
}
};
type PostResponse = {
data: typeof BLOG_DB[number];
message: string;
};
const postArticleSchema = Yup.object().shape({
title: Yup.string().required("Title is required!"),
content: Yup.string()
.required("Content is required!")
.max(
5000,
({ max }) => `Character limit exceeded! Max ${max} characters allowed!`
),
publishedTimestamp: Yup.number()
.required("Published timestamp is required!")
.lessThan(Date.now(), "Published timestamp cannot be in the future!"),
});
const createArticle: NextApiHandler<PostResponse> = async (req, res) => {
const data = validateRequest(req.body, postArticleSchema);
const newArticle = { ...data, id: BLOG_DB.length + 1 };
BLOG_DB.push(newArticle);
res
.status(201)
.json({ data: newArticle, message: "Article created successfully!" });
};
type DeleteResponse = {
data: typeof BLOG_DB[number];
message: string;
};
const deleteArticleById: NextApiHandler<DeleteResponse> = async (req, res) => {
const { id } = req.query;
if (!id) throw new createHttpError.BadRequest("Article id is required!");
const articleIndex = BLOG_DB.findIndex(
(article) => article.id === Number(id)
);
if (articleIndex < 0)
throw new createHttpError.NotFound(`Article with id ${id} not found!`);
BLOG_DB.splice(articleIndex, 1);
res.status(200).json({
data: BLOG_DB[articleIndex],
message: "Article deleted successfully!",
});
};
export default apiHandler({
GET: getArticle,
POST: createArticle,
DELETE: deleteArticleById,
});
The validateRequest()
function is a helper which takes in a yup schema and validates the JSON. It also returns the validated data with appropriate types. Yup throws a ValidationError if this validation fails which we're handling in our errorHandler()
.
// ./utils/yup.ts
import { ObjectSchema } from "yup";
import { ObjectShape } from "yup/lib/object";
export function validateRequest<T extends ObjectShape>(
data: unknown,
schema: ObjectSchema<T>
) {
const _data = schema.validateSync(data, {
abortEarly: false,
strict: true,
});
return _data;
}
You can test the API we just created using Postman or any other tool of your choice.
5. Wrapping up
Notice how clean and descriptive our business logic looks โจ
โ
res.json()
is called only when we need to send a success response.
โ
In all other cases we throw the appropriate error contructed by http-errors
and leave the rest on the parent function.
โ
Controllers are divided into functions and plugged into the API Route by their respective req.method
which is kind of reminiscent of how Express.js routes are defined.
Enforcing clean code practices in our projects increases code readability which matters a lot as the scale of the project starts increasing. ๐ช๐ฝ
I don't claim that this is the best way to achieve this goal so if you know of any other better ways, do share! ๐๐ฝ
I hope you liked this post and it'll help you in your Next.js projects. If it helped you with a project, I'd love to read about it in the comments. ๐
Shameless Plug
Checkout my recently published Modal library for React.js :3
react-lean-modal
6. Source Code
SneakySensei / Next-API-Handler-Example
Accompanying example project for my post on Dev.to
Create API Routes using apiHandler higher order function for consistency and clean code ๐ฅ
Features โก
-
res.json()
is called only when we need to send a success response. - Error responses are handled by apiHandler when we throw an error.
- Controllers are divided into functions and plugged into the API Route by their respective
req.method
which is kind of reminiscent of how Express.js routes are defined.
Technologies ๐งช
- Next.js
- TypeScript
- Yup
- http-errors
Installation ๐ฆ
First, run the development server:
```bash
npm run dev
# or
yarn dev
Try it out! ๐
Checkout the related Post on Dev.to ๐
https://dev.to/sneakysensei/nextjs-api-routes-global-error-handling-and-clean-code-practices-3g9p
Top comments (0)