High-Order Functions for Reusable Middleware in Next.js
Starting in version 12, Next.js introduced first-class support for middleware.
In this article, however, we'll look at how to create middleware using JavaScript high-order functions.
This approach could still be useful if you're
- not deploying to Vercel
- using Next.js 11 or an earlier version
- trying to learn another approach to writing middleware
We'll use a learn-by-example approach. We'll create a middleware that logs metrics, handles authentication, and validates the request body. I'll leave some exercise problems if you want some practice.
Let's get started.
Metrics
The first example I'll show is the simplest. I want to create a middleware that logs information about the HTTP request.
To create the higher-order function, we need to create a function that:
- takes as argument a handler function
- runs some business logic
- executes the handler function before returning
import {NextApiHandler, NextApiRequest, NextApiResponse} from 'next'
const withMetrics = (handler: NextApiHandler) => {
return async (req: NextApiRequest, res: NextApiResponse) => {
console.info('[api]', req.method, req.url)
await handler(req, res)
}
}
export default withMetrics
To see the middleware in action, we'll apply it to a simple API route: One that always sends a rocket emoji.
import { NextApiRequest, NextApiResponse } from 'next'
import withMetrics from '@lib/middleware/with-withMetrics.ts'
const handler = (req: NextApiRequest, res: NextApiResponse) => {
return res.status(200).send('🚀')
}
export default withMetrics(handler)
Now when we hit this API route, we will see the information about the request logged to the the server console.
Authentication
Another practical use case for middleware is authentication.
The topic of authentication
goes deep and beyond the scope of this article. We'll create a naive authentication implementation
for demonstration. Our implementation will check that the x-server-secret
header sent by
the client matches the server secret stored as an environment variable.
import {NextApiHandler, NextApiRequest, NextApiResponse} from 'next'
const withAuth = (handler: NextApiHandler) => {
return async (req: NextApiRequest, res: NextApiResponse) => {
// Reject unauthorized clients
if (req.headers['x-server-secret'] !== process.env.SERVER_SECRET) {
res.status(401).send('You did provide the secret value!')
return
}
await handler(req, res)
}
}
export default withAuth
It goes without saying you should follow a more secure practice when adding authentication to your own app.
We can now update /pages/api/rockets.ts
to only accept requests from clients that send the
correct x-server-secret
header value.
import { NextApiRequest, NextApiResponse } from 'next'
import withMetrics from '@lib/middleware/with-withMetrics.ts'
import withAuth from '@lib/middleware/with-auth.ts'
const handler = (req: NextApiRequest, res: NextApiResponse) => {
return res.status(200).send('🚀')
}
export default withMetrics(withAuth(handler))
If this looks confusing to you—functions passing in functions passing in functions— I was confused when I was learning it too. Try this. Read each higher-order function from left to right:
First, withMetrics
is run (the HTTP request info is logged).
Then, authentication (withAuth
) is run.
If the client is authenticated,
a 200 (OK)
is sent with a rocket emoji.
Otherwise, a 401 (Unauthorized)
is sent.
Validation
The third and final example we'll look at is a middleware for validating the request body in a POST request. For schema validation, I like Yup, so we'll be using it along with Lodash in the implementation, inspired by Bruno Antunes' Yup validation middleware.
npm install yup lodash
npm install -D @types/yup
We'll write a function withBody
that takes two arguments:
schemas
: Yup validation schemas.handler
: The same Next.js API handler we've come to love.
import {NextApiHandler, NextApiRequest, NextApiResponse} from 'next'
import {ObjectShape} from 'yup/lib/object'
import {has} from 'lodash'
import {ObjectSchema} from 'yup'
export type IHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
export type IValidationSchemas = Partial<
Record<IHttpMethod, ObjectSchema<ObjectShape>>
>
export function withBody(
schemas: IValidationSchemas,
handler: NextApiHandler,
) {
return async (req: NextApiRequest, res: NextApiResponse) => {
if (!has(schemas, req.method)) {
await handler(req, res)
return
}
const schema = schemas[req.method]
if (schema == null) {
const message = `Requires non-null validation schema to validate request body`
console.warn(message)
res.status(400).send(message)
return
}
try {
req.body = await schema.validate(req.body, {
abortEarly: process.env.NODE_ENV !== 'production',
})
await handler(req, res)
} catch (err) {
const message = `Failed to validate request body: ${err.message}`
console.warn(message)
res.status(400).send(message)
}
}
}
Now we'll go back to our API route and allow clients to create new
rockets, modeled by the IRocket
interface.
interface IRocket {
id: number
name: string
org: string
}
We'll use Yup to define our validation schemas.
const rocketValidationSchemas: IValidationSchemas = {
POST: yup.object().shape({
name: yup.string().required(),
org: yup.string().required(),
})
}
We will model the database with an array.
In your app, you'll want a database that is
const rockets: IRocket[] = [
{
id: 0,
name: 'Falcon V',
org: 'Space X',
},
{
id: 1,
name: 'New Glenn',
org: 'Blue Origin',
},
// And so on...
]
With these pieces, we'll add support for adding a rocket.
const handler = (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const newRocket = {
id: rockets.length,
...req.body,
}
rockets.push(newRocket)
return res.status(201).json(newRocket)
}
return res.status(200).send('🚀')
}
export default withMetrics(withAuth(withBody(rocketValidationSchemas, handler)))
Consider the execution of this API for POST requests:
- Log info about the request
- Authenticate the client
- If authenticated, validate the request body
- If body is valid, push the new rocket to the array
You've now looked at three examples of using higher-order functions for middleware in Next.js apps.
If you ever find yourself rewriting the same code in your API routes, think about how you could reuse code in a middleware.
Exercises
Here are some exercises to practice writing middleware. Each exercise harder than the previous.
- Create a middleware
withDevOnly
that sends a403 (Forbidden)
whenever running in production. Hint: checkprocess.env.NODE_ENV === 'development'
. - Create a middleware
withMethods
that takesIHttpMethod[]
and sends405 (Method Not Allowed)
if the request method is not included. - Create a middleware
withHandlers
that takes aPartial<Record<IHttpMethod, NextApiHandler>>
to supply implementations for eachIHttpMethod
. If the request method does not have an implementation, send405 (Method Not Allowed)
.