Skip to main content

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.

Middlewares Diagram Overview

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:

export default function useMyMiddleware(req, res, next) {
// 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:

// file: api/users/[id].[post].js
import { PersonSchema } from '../../schemas';

export default [
useSchemaValidation(PersonSchema),
createUser
];

function createUser(req, res, 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:

// file: api/users/[id].[post].js
export async function useValidation(req, res, 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:

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();
}

The diagram illustrating the described process:

Interruptions Diagram

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.

export async function useErrorHandler(req, res, next) {
try {
await next();
} catch (error) {
return res
.writeHead(500, { 'Content-Type': 'text/html' })
.end(error.message);
}
}

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

Returning Result Diagram

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.

export async function useCors(req, next) {
const res = await next();
if (!res) return;

res.headers.set('Access-Control-Allow-Methods', 'PUT');

return res;
}
Note

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);