Back to posts

Next.JS app router API middleware

2024/02/23

Recently I found this article explaining how to create an API wrapper function which works in a similar way as to an API-specific middleware, as the function runs at the start of the request pipeline. However, in one of my projects I've started using the app router with one of the changes impacting how API endpoints are created.

Creating an endpoint with the pages router

In the old pages router you can create an endpoint by exporting a handler which returns a response.

// pages/api/endpoint.js export default function handler(req, res) { res.status(200).json({ message: 'Hello world!' }) }

This handler should also be repsonsible for returning any errors such as invalid method requests, unauthenticated requests, etc. Copying the same code from endpoint to endpoint in a large project is tiresome, hence the usefulness of the API wrapper I mentioned earlier.

Creating an endpoint with the app router

In the app router, endpoints are officially called "Router Handlers". Each HTTP method has it's own handling function so Next.JS can take care of sending a 405 Method Not Allowed response. Super useful!

// app/api/endpoint/route.js export async function GET() { return Response.json({ message: 'Hello world!' }, { status: 200 }) }

However, what if you want to check user authentication on API routes, or have specific behaviours for a select few endpoints? Sure you could create a middleware.js and define your API routes, but if some methods are protected and others aren't at the same endpoint, things can get long and tedious quickly.

Creating an API wrapper for the app router

Following a similar approach as Jason did in his article, I've opted to create a wrapper that takes in a callback function and a set of options, which can wrap all API endpoints. Note, I'm using the experimental version 5.0.0-beta.12 of Auth.js as of writing this article, so some functionality changes may occur in the future.

// utils/apiHandler.js import { auth } from '@/auth' // optional options that can be passed into the creation of an endpoint // to alter the handler function, in this case if the route is not // protected, then it won't check the authentication const endpointOptions = { protected: true } export default function apiHandler(fn, options = endpointOptions) { return async (req) => { // check if request is authenticated const session = await auth(req) try { if (options.protected && session) { return Response.json({ message: 'Not authenticated' }, { status: 401 }) } // return the route handler return await fn(req, session) } catch (error) { // catch all errors console.error(error) return Response.json({ message: error.message }, { status: 500 }) } } }
// app/api/endpoint/route.js import apiHandler from '@/utils/apiHandler' export const GET = apiHandler( async (req, session) => { return Response.json({ message: 'Hello world!' }, { status: 200 }) }, { protected: false } ) export const DELETE = apiHandler(async (req, session) => { return Response.json({ message: 'Goodbye world!' }, { status: 200 }) })

In this example I create two handlers for GET and DELETE endpoints. The GET function is open as we specify that the route isn't protected, however to access the DELETE handler you need to be authenticated.

The handler can contain any code you require before a route handler get's called. If needed you can pass in the session as well so this does not need to be recalculated. This helps greatly in reducing the complexity of the route handlers as all shared logic can be stored inside the apiHandler function.