Middleware
Middleware functions are functions that have access to the request object (c.req), the response object (c.res), and the next function in the application’s request-response cycle.
Glasswork is built on top of Hono, which means you can use any Hono middleware directly. This provides a powerful and flexible way to handle cross-cutting concerns like authentication, logging, and error handling.
Using Middleware
You can apply middleware in two ways: globally or per-route.
Global Middleware
Global middleware is applied to all routes. This is typically done in your main application entry point (e.g., app.ts or main.ts) before defining routes.
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
const app = new Hono();
// Apply to all routes
app.use('*', logger());
app.use('*', cors());Per-Route Middleware
You can apply middleware to specific routes using the middleware property in the route() configuration.
import { cache } from 'hono/cache';
router.get('/stats', ...route({
summary: 'Get statistics',
middleware: [
cache({ cacheName: 'stats', cacheControl: 'max-age=3600' })
],
responses: { 200: StatsDto },
handler: async () => {
return statsService.getStats();
},
}));Concepts
If you are coming from frameworks like NestJS, you might be familiar with concepts like Guards, Interceptors, and Exception Filters. In Glasswork (and Hono), these are all implemented as Middleware.
Guards
Guards are responsible for determining whether a request should be handled by the route handler or not. They are typically used for authentication and authorization.
In Glasswork, a "Guard" is simply a middleware that checks a condition and throws an exception if it fails.
import type { MiddlewareHandler } from 'hono';
import { UnauthorizedException, ForbiddenException } from 'glasswork';
// Auth Guard
export const authGuard = (): MiddlewareHandler => {
return async (c, next) => {
const token = c.req.header('Authorization');
if (!token) {
throw new UnauthorizedException('Missing authentication token');
}
// Validate token logic...
const user = await validateToken(token);
c.set('user', user); // Attach user to context
await next();
};
};
// Role Guard
export const rolesGuard = (...roles: string[]): MiddlewareHandler => {
return async (c, next) => {
const user = c.get('user');
if (!user || !roles.includes(user.role)) {
throw new ForbiddenException('Insufficient permissions');
}
await next();
};
};Usage:
router.get('/admin', ...route({
middleware: [authGuard(), rolesGuard('admin')],
// ...
}));Interceptors
Interceptors have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. They make it possible to:
- bind extra logic before / after method execution
- transform the result returned from a function
- transform the exception thrown from a function
In Glasswork, you achieve this by wrapping the await next() call.
import type { MiddlewareHandler } from 'hono';
export const loggingInterceptor = (): MiddlewareHandler => {
return async (c, next) => {
console.log(`Before...`);
const start = Date.now();
await next(); // Execute the handler
const ms = Date.now() - start;
console.log(`After... ${ms}ms`);
c.header('X-Response-Time', `${ms}ms`);
};
};To transform the response, you can modify c.res after await next(), but be aware that if the response is already sent, you might be limited. Hono's context allows setting headers and modifying the response object.
Exception Filters
Exception filters are responsible for catching unhandled exceptions and formatting the response.
Glasswork has a built-in global exception filter that handles DomainExceptions (like NotFoundException, BadRequestException) and validation errors automatically.
However, you can create custom error handling middleware or use Hono's onError hook.
Custom Error Middleware:
app.onError((err, c) => {
if (err instanceof CustomDatabaseError) {
return c.json({
error: 'Database Error',
message: err.message
}, 500);
}
// Fallback to default handling
throw err;
});Or as a middleware that wraps execution:
export const errorFilter = (): MiddlewareHandler => {
return async (c, next) => {
try {
await next();
} catch (err) {
if (err instanceof MyCustomError) {
return c.json({ custom: 'error' }, 400);
}
throw err; // Re-throw for other handlers
}
};
};Built-in Middleware
Hono comes with a rich set of built-in middleware that you can use out of the box.
- Basic Auth: Basic authentication.
- Bearer Auth: Bearer token authentication.
- Compress: Gzip/Deflate compression.
- CORS: Cross-Origin Resource Sharing.
- ETag: ETag generation.
- Logger: Request logging.
- Pretty JSON: Pretty print JSON responses.
- Secure Headers: Security headers (Helmet equivalent).
- Timeout: Request timeout.
Refer to the Hono Middleware Documentation for the complete list and usage details.
Creating Custom Middleware
You can create any custom middleware by defining a function that returns a MiddlewareHandler.
import type { MiddlewareHandler } from 'hono';
export function myMiddleware(): MiddlewareHandler {
return async (c, next) => {
// 1. Logic before handler
const requestId = crypto.randomUUID();
c.set('requestId', requestId);
// 2. Pass control to next middleware/handler
await next();
// 3. Logic after handler
// (e.g., logging, cleanup)
};
}Extending Context
If your middleware sets custom variables on the context (like user or requestId), remember to extend the Hono type definition so TypeScript knows about them.
// src/types.d.ts or similar
import 'hono';
declare module 'hono' {
interface ContextVariableMap {
requestId: string;
user: { id: string; role: string };
}
}Now c.get('requestId') and c.get('user') will be strongly typed.
Furthermore, Glasswork automatically exposes these context variables in the route handler's argument object, allowing for direct destructuring:
router.get('/me', ...route({
middleware: [myMiddleware()],
responses: { 200: UserDto },
handler: ({ user, requestId }) => {
// user and requestId are directly available and typed
console.log(requestId);
return user;
},
}));