Next.js Middleware Chain (NMC) is a lightweight, zero-dependency next.js middleware solution. It is built on the idea that middleware functions are more flexible when they can be chained as needed during development.
NMC aims to provide a simple framework for writing methods that can be used with either Next.js API routes or SSR (getServerSideProps
) data fetching.
This library is still in an early stage of development. It has been tested it in a couple projects and should be stable, however there may be bugs or scenarios that have not been realized or planned for.
If you encounter a bug or problem, please open an issue.
If you have feedback or questions, please start a discussion.
Please be civil and abide by the code of conduct.
Next.js does a lot of great things. Middleware is not one of them. If you want to implement some functionality for all API routes (logging at start & finish, for instance) you have to either explicitly add it to all your routes or write your own reusable solution.
This gets doubly complex if you want to re-use the same functionality across API Routes and SSR data fetching methods, since the function APIs and behaviors differ in small but important ways.
It would be a lot easier to just write the middleware functions you want and then compose them however you need when building out new pages and routes.
That's the goal.
npm install nextjs-middleware-chain
This package exports a single named function createMiddleware
which takes two arguments, an array of middleware functions and an object of options to set globally for all middlware instances.
createMiddleware
returns a factory function which, when called, returns an instance of a Middleware
class which is used to run any subsequently chained functions.
This README will go into more detail for each part, but some initial context the setup & usage of NCM looks like:
// Setup in a `middleware.js` file...
import { createMiddleware } from 'nextjs-middleware-chain'
const mwFunction = (req, res, next) => {
console.log('Running function!')
return next()
}
const middlewareFunctionsArray = [ mwFunction ]
const optionsObject = { useChainOrder: true }
const mwFactory = createMiddleware(
middlewareFunctionsArray,
optionsObject
)
export default mwFactory
// Usage in an API or SSR route...
import mwFactory from './middleware'
/* ... api or ssr routeFunction(...) {...} ... */
export default mwFactory().mwFunction().finish(
routeFunction, // required
'Friendly Route Name' // optional
)
This repo also includes a Next.js sample "client" project which you can reference & run to test out the library. You can reference the setup & syntax for NMC in these files:
Any middleware functions you create will be made available to NMC by adding them to this array, and passing this array as the first argument to createMiddleware
.
The default options will apply globally unless they are overwritten. You can overwrite options:
- globally: by passing an object as the second argument to the
createMiddleware
function. - per-route: by passing an object as the only argument to the middleware factory responsible for creating an instance of the middleware.
mwFactory(optionsObject).mwFunction().finish(...)
Property | Type | Default | Purpose |
---|---|---|---|
useChainOrder | bool | true | Run middleware functions in the order they are used in the chain. When false this will always run the middleware function in the same order - the order they were added to the functions array passed to createMiddleware . |
useAsyncMiddleware | bool | true | Run middleware functions asynchronously. When false middleware functions will run synchronously. |
reqPropName | string | nmc |
The name of the decoration property added to the req object for all routes. You can rename this if it causes a collision or of you don't like these letters. |
onMiddlewareStart | function | (req) => {} | Run before starting the middleware chain. Receives the decorated req object as an argument. |
onMiddlewareComplete | function | (req) => {} | Run after all middleware is complete (or when next() is called with 'end' or 'route' ) and before the Next.js route is run. Receives the decorated req object as an argument. |
onRouteComplete | function | (req) => {} | Runs after the Next.js route has been run (or skipped). Receives the decorated req object as an argument. |
All middleware functions will receive 3 arguments:
req
res
next
req
and res
are provided by the Next.js route. They will be passed by reference to all middleware functions, so can be mutated throughout the chain. Be aware than if you are using a chain function in both API and SSR contexts you may not have access to certain Next.js-provided decorations, which are only added for API routes:
next
is a custom function for NMC. Each middleware function must return next()
in order to continue running the middleware.
A basic middleware function looks like:
const middlewareChainFn = (req, res, next) => {
// Do things here...
return next()
}
You can break the chain in one of three ways.
To skip any remaining middleware functions and execute the Next.js route:
return next('route')
To skip any remaining middleware function and ignore the Next.js route:
- From API routes: End the response via the
res
object, and return nothing.res.status(###).json({ message: '...' })
- From SSR routes: Return with the keyword
'end'
and a payload object.return next('end', { ...payload })
- The payload object is required - it will be the
return
fromgetServerSideProps
so should be a standard Next.js getServerSideProps response.
const isAuthorized = (req, res, next) => {
// Some authorization function...
const authorized = getAuthorization(req)
if (req.nmc.type === 'api' && !authorized) {
// End the request if it is an API route
res.status(401).json({error: 'unauthorized', message: '...'})
} else if (req.nmc.type === 'ssr' && !authorized) {
// End the request if it is an SSR route
return next('end', {
redirect: {
destination: '/',
permanent: false,
}
})
} else {
// Skip remaining middleware and continue with the Next.js route.
return next('route')
}
}
NMC will add a custom property, nmc
, (the prop name is configurable in options) to the req
received by each middleware function. This object will contain:
Property | Type | Value |
---|---|---|
id |
string |
a random guid unique to each middleware instance |
name |
string |
finalFuncName if provided, otherwise finalFunc.name , otherwise '' |
type |
string |
api or ssr - the type of route the middleware is being run from* |
context |
object |
The Next.js context object provided to SSR routes. Will be {} for API routes. |
* This determination is made based on the arguments the middleware runner receives from Next.js, which will either be (req, res) => {...}
for API routes, or (context, undefined) => {...}
for SSR routes.
You can run middleware functions yourself, if desired, but you will have to handle the returns from each as needed for your use-case:
const common = async (req, res, next) => {
const aReturn = fnA(req, res, next)
const bReturn = await fnB(req, res, next)
const authReturn = unauthorized(req, res, next)
if (authReturn) {
return authReturn
}
}
You can create pre-built chains of middleware if certain functions are commonly used together, or should always be run in a specific order:
// Creating a pre-built chain in middleware.js
const mwFactory = createMiddleware(
middlewareFunctionsArray,
optionsObject
)
const preBuiltChain = mwFactory().fnA().fnB().fnC()
export { preBuiltChain }
export default mwFactory
// Using a pre-built chain
import { preBuiltChain } from 'nextjs-middleware-chain'
/* ... api or ssr routeFunction(...) {...} ... */
export default preBuiltChain.otherMiddleware().finish(routeFunction)
Note that when you import & use the preBuiltChain
it is already an instance of Middleware
and so should be accessed as an object, not run as a method.
- Add automatic versioning & releases
- Add additional unit tests
- Move to TypeScript