How to Write Actual API Middleware for Next.js
Whenever I have to implement a web client for an app, or proof of concept of some sort, I instinctively reach for Next.js. A couple of years ago, I had to implement an app around the time Next.js version 12 came out, and I was pumped to hear about all the fancy bells and whistles they released. One of the most exciting features to me was the release of API Middleware.
My excitement was short-lived, however. Their implementation has the same name and is conceptually similar, but it feels like they re-invented the wheel, making it a brick dipped into vaseline in the process.
What Problem does Middleware solve?
The Next.js API routes are, without a doubt, one of the most convenient ways to add an API to your client. All you need to do is create a new file inside an api
directory and start writing your function.
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}
This simplicity is nice to get started with, but it pushes complexity down to the implementation level.
For example, you’ll need to handle the request type branching in the implementation.
export default function handler(req, res) {
if (req.method === 'POST') {
// Process a POST request
} else {
// Handle any other HTTP method
}
}
Now, what if we want to make sure the user is authenticated? We’d need to handle that inside the route handler as well.
export default function handler(req, res) {
if (!isAuthenticated(req)) {
res.status(401).send("Unauthorized")
}
// Implementation
}
Once you go down this rabbit hole, you’ll notice that the conditions at the top of your handler implementation start piling up, and what’s worse, you’re doing a lot of copy-pasting from handler to handler. It becomes a complete mess.
How is this Problem Usually Solved?
This problem is very elegantly solved by frameworks such as Express.
Our code can look something like the one below:
app.post(
'/hello',
requireAuth,
validatePayload,
doUsefulStuff,
respond,
);
It essentially allows you to stack any number of re-usable functions on top of each other as long as they implement the following interface:
- The function needs to have a parameter called
next
which is itself a function. - After your custom logic runs, you either call
next()
to move to the next function in the chain or respond to the request right away.
function myMiddleware(req, res, next) {
// Do some checking
next()
}
Now you can just enable this middleware by adding them to the list and disabling them by removing or commenting them out.
It is a very clear syntax, and the code is reusable.
How is Middleware Solved by Next.js?
If you look at the Next.js documentation, you’ll see they want you to create a middleware.js
file where you’ll implement your middleware. Already I’m not too fond of this, but let’s move on.
// middleware.js
export function middleware(request) {
return response
}
After you implement your middleware, you should export a config object that tells Next.js where this middleware should run.
// middleware.js
export const config = {
matcher: '/api/:your-endpoint-here*'
}
In this config object, you specify matchers that also support regular expressions.
If you’re still unsatisfied, you can try conditional statements within your middleware declaration to apply your custom logic that way.
// middleware.js
export function middleware(request) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
Now, here’s a couple of reasons why I don’t like this approach:
- Having a dedicated
middleware.js
file that contains the middleware implementation seems clunky. I want my middleware to live where my routes are defined. - The
config
andmatcher
approach feels super unnecessary. Why do I have to write matchers and regex to run my middleware? I already set up my routes with the directory structure (the main reason I chose Next.js in the first place); now they’re telling me I have to go to another vaguely related place to write matchers or conditionals.
There are some cases where I can see this approach being practical, like using middleware on an application level that runs for every request, etc.
That said, I’m not a fan of the design of this feature. I want to define my middleware where I define my routes, stack them up nicely like pancakes, and make them simple for me to maintain.
Custom Middleware for API Routes
Next.js recently released Route Handlers, but since most people (me included) are still using API Routes on most of our projects, I’ll start by covering the implementation for API Routes.
The source code is available on GitHub, and in the examples below I’ll be using simplified JavaScript, close to pseudo-code but still functional.
The code below illustrates how we would like our route to work.
// src/pages/api/hello.js
const middleware_1 = async (req, res, next) => {
console.log('Running middleware 1')
next()
};
const middleware_2 = async (req, res, next) => {
console.log('Running middleware 2')
next()
};
const middleware_3 = async (req, res, next) => {
console.log('Running middleware 3')
next()
};
const middleware_4 = async (req, res, next) => {
console.log('Running middleware 4')
next()
};
const hello = async (req, res) => {
res.status(200).json({ message: 'Hello World.' })
};
export default handler(
middleware_1,
middleware_2,
middleware_3,
middleware_4,
hello,
);
At any point in the middleware chain, you should be able to respond using the res
object instead of calling the next()
function, and the chain stops executing. Also, naturally, the middleware should be declared elsewhere, made reusable, and imported into the route.
In order to achieve this functionality, all we need to do is implement the handler
function we’re calling here.
You can see an example implementation below, and you’ll find the source code here.
// src/pages/middleware/handler.js
const execMiddleware = async (
req,
res,
middleware,
index = 0,
) => {
if (res.headersSent || !middleware[index]) return
if (typeof middleware[index] !== 'function') {
res.status(500).end('Middleware must be a function!')
throw new Error('Middleware must be a function!')
}
await middleware[index](req, res, async () => {
await execMiddleware(req, res, middleware, index + 1)
})
}
export const handler =
(...middleware) =>
async (req, res) => {
await execMiddleware(req, res, middleware)
}
As an added bonus, you can find a allowMethods
middleware here that you can use on your routes like this:
// src/pages/api/hello.js
export default handler(
allowMethods(['GET', 'PUT']),
hello,
);
Custom Middleware for Route Handlers
Route Handlers is the latest API implementation approach from Next.js that they recommend we adopt moving forward.
Once again, a feature that causes mixed feelings in me. On the one hand, I like that they now require a named export per HTTP method (GET, POST, PUT, etc.), but they also made a change I’m not a massive fan of. Namely, having to explicitly return
the response value from within the handler.
Just like above, the source code is available on GitHub, and I’ll be using simplified JavaScript to illustrate the feature we’re implementing.
The code below shows how we’d like our route to work.
// src/app/api/hello/route.js
const middleware_1 = async (req, next) => {
console.log('Running middleware 1')
next()
};
const middleware_2 = async (req, next) => {
console.log('Running middleware 2')
next()
};
const middleware_3 = async (req, next) => {
console.log('Running middleware 3')
next()
};
const middleware_4 = async (req, next) => {
console.log('Running middleware 4')
next()
};
const hello = async (req) => {
return NextResponse.json({ data: 'Hello World' })
};
export const GET = handler(
middleware_1,
middleware_2,
hello,
);
export const POST = handler(
middleware_3,
middleware_4,
hello,
);
Once again, at any point in the middleware chain, you should be able to respond by returning a response instead of calling the next()
function, and the chain stops executing.
Below is the example implementation of the handler
function; you can find the source code here.
// src/app/middleware/handler.js
export const handler =
(...middleware) =>
async (request) => {
let result
for (let i = 0; i < middleware.length; i++) {
let nextInvoked = false
const next = async () => {
nextInvoked = true
};
result = await middleware[i](request, next);
if (!nextInvoked) {
break
}
}
if (result) return result
throw new Error('Your handler or middleware must return a NextResponse!')
}
Conclusion
Obviously a matter of preference; however, we could successfully monkey patch in our middleware implementation into Next.js in under 50 lines of code, and that’s a win in my book.
As for the direction Next.js is taking, I’m crossing my fingers that we’re just on a detour and we’ll go back to a simple, clean, and easy developer experience. I fell in love when I saw how smooth their DX was a few years ago, and I haven’t abandoned hope yet.
As for the middleware pattern, feel free to use the code in any project you see fit. It is a great way to break down logic and stack it up in bite-sized pieces.
Stay curious, and happy coding!