Skip to content

Middleware

Middleware functions have access to the request context and can control the request flow. They’re perfect for cross-cutting concerns like logging, authentication, and error handling.

Middleware receives the context and a next function:

import { Middleware } from 'shokupan';
const logger: Middleware = async (ctx, next) => {
console.log(`${ctx.method} ${ctx.path}`);
const start = Date.now();
const result = await next();
console.log(`${ctx.method} ${ctx.path} - ${Date.now() - start}ms`);
return result;
};
app.use(logger);

Apply middleware to all routes:

// Logging
app.use(async (ctx, next) => {
console.log(`${ctx.method} ${ctx.path}`);
return next();
});
// Request ID
app.use(async (ctx, next) => {
ctx.state.requestId = crypto.randomUUID();
return next();
});
// Add routes after middleware
app.get('/', (ctx) => {
return { requestId: ctx.state.requestId };
});

Apply middleware to specific routes:

const authenticate = async (ctx, next) => {
const token = ctx.headers.get('authorization');
if (!token) {
return ctx.json({ error: 'Unauthorized' }, 401);
}
// Validate token and attach user
ctx.state.user = { id: '123', name: 'Alice' };
return next();
};
// Apply to single route
app.get('/protected', authenticate, (ctx) => {
return { user: ctx.state.user };
});
// Apply to multiple handlers
app.post('/admin',
authenticate,
checkAdmin,
(ctx) => {
return { admin: true };
}
);

Apply middleware to all routes in a router:

import { ShokupanRouter } from 'shokupan';
const apiRouter = new ShokupanRouter();
// Middleware for all routes in this router
apiRouter.use(authenticate);
apiRouter.get('/users', (ctx) => ({ users: [] }));
apiRouter.get('/posts', (ctx) => ({ posts: [] }));
app.mount('/api', apiRouter);

Use the @Use decorator for controllers:

import { Use } from 'shokupan';
// On entire controller
@Use(authenticate)
export class AdminController {
@Get('/dashboard')
getDashboard() { }
}
// On specific methods
export class UserController {
@Get('/')
@Use(rateLimit)
getUsers() { }
}

Middleware can catch and handle errors:

const errorHandler: Middleware = async (ctx, next) => {
try {
return await next();
} catch (error) {
console.error('Error:', error);
return ctx.json({
error: 'Internal Server Error',
message: error.message
}, 500);
}
};
// Add early in middleware chain
app.use(errorHandler);

Track request duration:

const timing: Middleware = async (ctx, next) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
// Add timing header
ctx.set('X-Response-Time', `${duration}ms`);
return result;
};
app.use(timing);

Create an authentication middleware:

const authenticate: Middleware = async (ctx, next) => {
const token = ctx.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return ctx.json({ error: 'No token provided' }, 401);
}
try {
// Verify JWT token
const user = await verifyToken(token);
ctx.state.user = user;
return next();
} catch (error) {
return ctx.json({ error: 'Invalid token' }, 401);
}
};

Add unique request IDs:

const requestId: Middleware = async (ctx, next) => {
const id = crypto.randomUUID();
ctx.state.requestId = id;
ctx.set('X-Request-ID', id);
return next();
};
app.use(requestId);
app.get('/', (ctx) => {
console.log(`Request ID: ${ctx.state.requestId}`);
return { requestId: ctx.state.requestId };
});

Simple CORS middleware (use CORS plugin for production):

const cors: Middleware = async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (ctx.method === 'OPTIONS') {
return ctx.status(204);
}
return next();
};
app.use(cors);

Apply middleware conditionally:

const conditionalAuth: Middleware = async (ctx, next) => {
// Skip auth for public endpoints
if (ctx.path.startsWith('/public')) {
return next();
}
// Require auth for all other endpoints
return authenticate(ctx, next);
};
app.use(conditionalAuth);

Middleware executes in the order it’s added:

// ✅ Correct order
app.use(errorHandler); // 1. Error handling first
app.use(logger); // 2. Logging
app.use(authenticate); // 3. Authentication
app.use(rateLimit); // 4. Rate limiting
// Routes
app.get('/', handler);
// Request flow:
// errorHandler → logger → authenticate → rateLimit → handler
// Response flow (reversed):
// handler → rateLimit → authenticate → logger → errorHandler

Middleware can modify responses:

const addHeaders: Middleware = async (ctx, next) => {
const result = await next();
// Add custom headers to response
ctx.set('X-Powered-By', 'Shokupan');
ctx.set('X-Version', '1.0.0');
return result;
};
app.use(addHeaders);

Use ctx.state to share data between middleware:

const loadUser: Middleware = async (ctx, next) => {
const userId = ctx.query.get('userId');
if (userId) {
ctx.state.user = await fetchUser(userId);
}
return next();
};
const checkPermissions: Middleware = async (ctx, next) => {
if (!ctx.state.user?.isAdmin) {
return ctx.json({ error: 'Forbidden' }, 403);
}
return next();
};
app.get('/admin', loadUser, checkPermissions, (ctx) => {
return { user: ctx.state.user };
});

Shokupan provides many built-in middleware plugins:

import {
Cors,
Compression,
RateLimit,
SecurityHeaders
} from 'shokupan';
app.use(Cors());
app.use(Compression());
app.use(RateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(SecurityHeaders());

See the Plugins section for more details.