Next.js API routes – Global Error Handling and Clean Code Practices

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 rout…


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Snehil

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).

Central Error Handling GIF

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

  1. Project Setup
  2. API Handler Higher Order Function
  3. Global Error Handler
  4. Creating example API route
  5. Wrapping up
  6. 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 Errors 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.
Run in Postman

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

GitHub logo 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 🧪

Installation 📦

First, run the development server:

```bash
npm run dev
# or
yarn dev

Try it out! 🚀

Run in Postman

Checkout the related Post on Dev.to đź“–

https://dev.to/sneakysensei/nextjs-api-routes-global-error-handling-and-clean-code-practices-3g9p


This content originally appeared on DEV Community 👩‍💻👨‍💻 and was authored by Snehil


Print Share Comment Cite Upload Translate Updates
APA

Snehil | Sciencx (2022-10-15T12:53:22+00:00) Next.js API routes – Global Error Handling and Clean Code Practices. Retrieved from https://www.scien.cx/2022/10/15/next-js-api-routes-global-error-handling-and-clean-code-practices/

MLA
" » Next.js API routes – Global Error Handling and Clean Code Practices." Snehil | Sciencx - Saturday October 15, 2022, https://www.scien.cx/2022/10/15/next-js-api-routes-global-error-handling-and-clean-code-practices/
HARVARD
Snehil | Sciencx Saturday October 15, 2022 » Next.js API routes – Global Error Handling and Clean Code Practices., viewed ,<https://www.scien.cx/2022/10/15/next-js-api-routes-global-error-handling-and-clean-code-practices/>
VANCOUVER
Snehil | Sciencx - » Next.js API routes – Global Error Handling and Clean Code Practices. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/10/15/next-js-api-routes-global-error-handling-and-clean-code-practices/
CHICAGO
" » Next.js API routes – Global Error Handling and Clean Code Practices." Snehil | Sciencx - Accessed . https://www.scien.cx/2022/10/15/next-js-api-routes-global-error-handling-and-clean-code-practices/
IEEE
" » Next.js API routes – Global Error Handling and Clean Code Practices." Snehil | Sciencx [Online]. Available: https://www.scien.cx/2022/10/15/next-js-api-routes-global-error-handling-and-clean-code-practices/. [Accessed: ]
rf:citation
» Next.js API routes – Global Error Handling and Clean Code Practices | Snehil | Sciencx | https://www.scien.cx/2022/10/15/next-js-api-routes-global-error-handling-and-clean-code-practices/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.