Published: April 13 2023

Next.js 13 Middleware for Authentication and Error Handling on API Routes

Tutorial built with Next.js 13.2.4

This is a quick post to show how to add authentication and error handling middleware to all API routes of a Next.js 13 application.

The code snippets in this post are taken from a Next.js + MongoDB auth tutorial I posted recently, the full project and documentation is available at Next.js 13 + MongoDB - User Registration and Login Tutorial with Example App.

API Routes in Next.js

In Next.js the /pages/api folder contains all API endpoints which are routed based on file name, for example the file /pages/api/users/index.js is automatically mapped to the route /api/users. An API route handler exports a default function that is passed the HTTP request (req) and response (res) objects.

For more info on Next.js API routes see https://nextjs.org/docs/api-routes/introduction.


Example Next.js API Route Handler

Here's a basic example of an API route handler that returns a list of users to HTTP GET requests.

You can see that the handler() function is passed the req and res objects for accessing details of the request and setting properties of the response.

import { usersRepo } from 'helpers/api';

export default handler;

function handler(req, res) {
    switch (req.method) {
        case 'GET':
            return getUsers();
        default:
            return res.status(405).end(`Method ${req.method} Not Allowed`)
    }

    function getUsers() {
        const users = usersRepo.getAll();
        return res.status(200).json(users);
    }
}


Next.js API Handler

Path: /helpers/api/api-handler.js

To add support for middleware I created the below apiHandler() wrapper function, it accepts an API handler object with a method for each supported HTTP method (e.g. get(), post(), put(), delete() etc). If a request is received for an unsupported HTTP method a 405 Method Not Allowed response is returned.

It enables adding Next.js middleware globally to API routes by executing middleware functions (e.g. jwtMiddleware(req, res)) before the route handler for the HTTP method (handler[method](req, res)).

Errors are also handled globally by wrapping the middleware functions and handler in a try/catch block and calling the errorHandler(err, res) on exception.

import { errorHandler, jwtMiddleware } from 'helpers/api';

export { apiHandler };

function apiHandler(handler) {
    return async (req, res) => {
        const method = req.method.toLowerCase();

        // check handler supports HTTP method
        if (!handler[method])
            return res.status(405).end(`Method ${req.method} Not Allowed`);

        try {
            // global middleware
            await jwtMiddleware(req, res);

            // route handler
            await handler[method](req, res);
        } catch (err) {
            // global error handler
            errorHandler(err, res);
        }
    }
}
 

Users [id] API Route Handler

Path: /pages/api/users/[id].js

A dynamic API route handler created with the apiHandler() function, it handles HTTP requests with any value as the [id] parameter (i.e. /api/users/*). The user id parameter is attached by Next.js to the req.query object which is accessible to the route handler.

The route handler supports HTTP GET, PUT and DELETE requests by passing an object with those method names to the apiHandler().

import { apiHandler, usersRepo } from 'helpers/api';

export default apiHandler({
    get: getById,
    put: update,
    delete: _delete
});

async function getById(req, res) {
    const user = await usersRepo.getById(req.query.id);

    if (!user) throw 'User Not Found';

    return res.status(200).json(user);
}

async function update(req, res) {
    await usersRepo.update(req.query.id, req.body);
    return res.status(200).json({});
}

async function _delete(req, res) {
    await usersRepo.delete(req.query.id);
    return res.status(200).json({});
}
 

Next.js JWT Auth Middleware

Path: /helpers/api/jwt-middleware.js

The JWT authentication middleware uses the express-jwt library to validate JWT tokens in requests sent to protected API routes, if the token is invalid an error is thrown which causes the global error handler to return a 401 Unauthorized response. The JWT middleware is added to the Next.js request pipeline in the API handler wrapper function.

The register and authenticate routes are made public by passing them to the unless() method of the express-jwt library. For more info on express-jwt see https://www.npmjs.com/package/express-jwt.

import { expressjwt } from 'express-jwt';
import util from 'util';
import getConfig from 'next/config';

const { serverRuntimeConfig } = getConfig();

export { jwtMiddleware };

function jwtMiddleware(req, res) {
    const middleware = expressjwt({ secret: serverRuntimeConfig.secret, algorithms: ['HS256'] }).unless({
        path: [
            // public routes that don't require authentication
            '/api/users/register',
            '/api/users/authenticate'
        ]
    });

    return util.promisify(middleware)(req, res);
}
 

Next.js Global Error Handler

Path: /helpers/api/error-handler.js

The global error handler is used catch all errors and remove the need for duplicated error handling code throughout the Next.js API. It's added to the request pipeline in the API handler wrapper function.

By convention errors of type 'string' are treated as custom (app specific) errors, this simplifies the code for throwing custom errors since only a string needs to be thrown (e.g. throw 'Username or password is incorrect'), if a custom error ends with the words 'not found' a 404 response code is returned, otherwise a standard 400 error response is returned.

If the error is an object with the name 'UnauthorizedError' it means JWT token validation has failed so a HTTP 401 unauthorized response code is returned with the message 'Invalid Token'.

All other (unhandled) exceptions are logged to the console and return a 500 server error response code.

export { errorHandler };

function errorHandler(err, res) {
    if (typeof (err) === 'string') {
        // custom application error
        const is404 = err.toLowerCase().endsWith('not found');
        const statusCode = is404 ? 404 : 400;
        return res.status(statusCode).json({ message: err });
    }

    if (err.name === 'UnauthorizedError') {
        // jwt authentication error
        return res.status(401).json({ message: 'Invalid Token' });
    }

    // default to 500 server error
    console.error(err);
    return res.status(500).json({ message: err.message });
}

 


Need Some NextJS Help?

Search fiverr for freelance NextJS developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by