Middlewares
Middlewares are functions that execute before the main API handler. They are used for performing tasks such as authentication verification, request logging, error handling, and more. You can apply middleware to the entire application, to specific routes, or to a scope of routes within a folder.
Usage
For more examples, see the examples folder.
File Location and Scopes
To implement middleware, create a file named middleware.js
(or .ts, .cjs, etc.) and place it in the api folder.
This file will be executed for every request.
├── api/
│ └── middleware.js <-
├── server.js
└── package.json
To limit the scope, place the middleware file inside a specific folder. For example:
├── api/
│ ├── users/
│ │ ├── ...
│ │ └── middleware.js <-
│ └── index.js
...
In this case, the middleware will only execute for endpoints under /users/*
.
Middleware Declaration
Each middleware receives all the arguments passed from the initial request handler, with the last argument always
being the next function. This is provided automatically, giving you control over the incoming arguments.
Depending on the platform, you can utilize req
and res
objects, just the req
object, or any other arguments
you need.
Here's an example of a middleware implementation in a Node.js/Express.js API:
- JavaScript
- TypeScript
export default function useMyMiddleware(req, res, next) {
// Code before
// ...
await next(); // <-- The router's code or next middleware (on nested levels)
// ...
// Code after
}
import { IncomingMessage, ServerResponse } from 'node:http';
import type { NextFunction } from 'node-file-router';
export default function useMyMiddleware(
req: IncomingMessage,
res: ServerResponse,
next: NextFunction
) {
// Code before
// ...
await next(); // <-- The router's code or next middleware (on nested levels)
// ...
// Code after
}
List of Middlewares
To compose multiple middlewares, arrange them in an array and export it as the default. They will execute sequentially in the order they are listed.
// file: api/middlewares.js
import { useLogger, useAuthGuard, useErrorHandler } from '../middlewares';
export default [
useErrorHandler,
useLogger,
useAuthGuard
];
Usage in File Routes
To use middlewares in file routes, just return an array with middlewares before the request handler. For instance:
- Pure Node.js / Express.js
- Bun
// file: api/users/[id].[post].js
import { PersonSchema } from '../../schemas';
export default [
useSchemaValidation(PersonSchema),
createUser
];
function createUser(req, res, routeParams) {
// ...
}
// file: api/users/[id].[post].js
import { PersonSchema } from '../../schemas';
export default [
useSchemaValidation(PersonSchema),
createUser
];
function createUser(req, routeParams) {
// ...
}
In this example setup, useSchemaValidation(PersonSchema)
serves as a middleware that validates the request
against PersonSchema
. Once the request passes through this middleware, it then proceeds to the createUser function,
which handles the actual request logic.
In the context of a route handler, route parameters become accessible to middleware functions after the next
argument.
For example:
- Pure Node.js / Express.js
- Bun
// file: api/users/[id].[post].js
export async function useValidation(req, res, next, routeParams) {
// ...
}
export default [
useValidation,
// ...
];
// file: api/users/[id].[post].js
export async function useValidation(req, next, routeParams) {
// ...
}
export default [
useValidation,
// ...
];
Interrupting the Chain
To interrupt the chain of middlewares, call a return before await next()
or throw an error. If you're interested
in how to handle such errors inside other middlewares, see the Error Handling section.
This is an example code of a middleware that checks if the user is authenticated. All middlewares after this one including the matched request handler, will not be executed.
Here's an example of an authentication middleware, demonstrating how to interrupt the middleware chain:
- Pure Node.js
- Express.js
- Bun
export async function useAuth(req, res, next) {
const auth = req.headers.authorization;
// Some logic to check if the user is authenticated
if (!auth) {
return res
.writeHead(401, { 'Content-Type': 'text/html' })
.end('Not Authorized');
}
await next();
}
export async function useAuth(req, res, next) {
const auth = req.headers.authorization;
// Some logic to check if the user is authenticated
if (!auth) {
return res.status(401).send('Unauthorized');
}
await next();
}
export async function useAuth(req, next) {
const auth = req.headers.get('Authorization');
// Some logic to check if the user is authenticated
if (!auth) {
return new Response('Unauthorized', { status: 401 });
}
await next();
}
The diagram illustrating the described process:
Error Handling
As mentioned previously, you can interrupt the middleware chain by throwing an error. Occasionally, other middlewares
or the request handler itself might accidentally encounter an error. To manage these errors, simply wrap await next()
in a try/catch block within the middleware. Place this logic early in the middleware chain to ensure it captures
errors from all subsequent middlewares and request handlers.
- Pure Node.js
- Express.js
- Bun
export async function useErrorHandler(req, res, next) {
try {
await next();
} catch (error) {
return res
.writeHead(500, { 'Content-Type': 'text/html' })
.end(error.message);
}
}
export async function useErrorHandler(req, res, next) {
try {
await next();
} catch (error) {
res.status(500).send(error.message);
}
}
export async function useErrorHandler(req, next) {
try {
await next();
} catch (error) {
return new Response(error.message, { status: 500 });
}
}
Shared State (Request Context)
You can establish a shared state between middlewares and request handlers. To achieve this, pass your state object (or multiple ones) to the main request handler. For instance:
// ...
const server = http.createServer((req, res) => {
const ctx = {};
useFileRouter(req, res, ctx);
});
// ...
This state can then be accessed in any middleware:
export default async function useMyMiddleware(req, res, ctx, next) {
// ...
ctx.myState = 'some value';
await next();
// ...
}
Similarly, it can be accessed in the request handler:
export default function createUser(req, res, ctx) {
// ...
console.log(ctx.myState); // 'some value'
// ...
}
The node-file-router
is designed for flexibility in its interface. You can introduce any number of arguments
at the beginning, and these arguments will remain accessible throughout all stages of the processing chain.
Returning Result
In some situations, you may need to use an object as the final outcome of middleware, or modify one arrived from a request handler.
A good example is altering a Response
object in Bun.js
. To do this, just return a value from the middleware. This value will
then be the result of the await next()
call.
- JavaScript
- TypeScript
export async function useCors(req, next) {
const res = await next();
if (!res) return;
res.headers.set('Access-Control-Allow-Methods', 'PUT');
return res;
}
import type { NextFunction } from 'node-file-router';
export async function useCors(req: Request, next: NextFunction<Response>) {
const res = await next();
if (!res) return;
res.headers.set('Access-Control-Allow-Methods', 'PUT');
return res;
}
Remember to return the result of await next()
in the subsequent middlewares if you wish to utilize the outcome from
later stages. In other scenarios, returning a result from next
is not mandatory.
The value from middleware is returned from the useFileRouter
function, so you can use it in the same way as you would use the
result from the request handler.
const useFileRouter = await initFileRouter();
// ...
const response = await useFileRouter(req);