Middleware Use Cases
Learning objectives
- You know of some of the use cases for middleware.
Middleware functions are used to process requests made to the server. As the processing is not limited by path or request method, middleware allows adding cross-cutting functionality that is needed in all requests. Such functionality includes logging and timing requests, logging errors, controlling access to resources, etc.
Common functionality
Web application frameworks such as Hono also provide common functionality directly from the framework. As an example, see the built-in logger middleware documentation for Hono. Some of the examples here are also implemented in the framework itself.
Logging and timing requests
One potential use case is logging requests, potentially with timing them. This would be achieved by creating a middleware function that stores the time at the start of the request, processes the request, and then prints the time it took to process the request. The following example shows how this could be implemented.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const app = new Hono();
app.use("*", async (c, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${c.req.method} ${c.req.path} - ${ms}ms`);
});
app.get("/", (c) => c.text("Hello middlewares!"));
Deno.serve(app.fetch);
When we make a request to the server, the server logs the request method, path, and the time it took to process the request. An as example, the output could be as follows.
GET / - 0ms
Logging errors
Another usecase would be logging errors on the server. This is already done by default in Hono, but we could also implement this ourselves, potentially sending information about the errors to another service.
The following example shows how this could be implemented. The middleware wraps the next
call into a try-catch -block, which catches errors thrown in the subsequent middleware. The errors are then logged to the console, where they can be inspected. It would also be possible to send the errors to another service, such as a logging service.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const app = new Hono();
app.use("*", async (c, next) => {
try {
await next();
} catch (e) {
// send the error to another service
console.log(e);
}
});
app.get("/", (c) => c.text("Hello middlewares!"));
Deno.serve(app.fetch);
Handling users
Middlewares could also be used to implement functionality for retrieving users from the database based on session data. Such middleware could look something like the following -- the following assumes that there is a function getUserFromSession
that is used to retrieve a user from the database based on a session identifier (i.e., similar to the one that was built in the chapter on User Management).
The middleware places the user as a property of the context, which allows the use of the user in functions that are handled after the middleware.
app.use("*", async (c, next) => {
c.user = await sessionService.getUserFromSession(c);
await next();
});
Controlling access to resources
With information on the user in the context, it is also possible to implement access control to resources. As an example, we could implement a middleware that checks if the user is authenticated, and if not, returns an error. This could be implemented as follows.
app.use("/secrets/:id", async (c, next) => {
const authenticated = c.user;
if (!authenticated) {
return c.text("You have not authenticated!", 401);
}
await next();
});
With the above middlewares in place, the route mapping for the resource would be as follows -- the the function would focus on the key functionality, while the middlewares would handle access control.
app.get(
"/secrets/:id",
(c) => c.text("You can see this resource."),
);