Middleware
Complete guide to understanding and creating middleware in Laju Framework.
Introduction
Middleware functions are functions that have access to the request and response objects. They can execute code, modify request/response objects, and control the flow of the request-response cycle.
What Middleware Can Do
- Execute any code
- Modify request and response objects
- End the request-response cycle
- Validate input data
- Check authentication/authorization
- Log requests
- Rate limiting
- Transform data
HyperExpress vs Express.js
⚠️ CRITICAL DIFFERENCE
HyperExpress supports TWO middleware patterns:
// ❌ Express.js style (DON'T USE)
app.use((req, res, next) => {
console.log('Middleware');
next(); // ❌ This doesn't exist in HyperExpress
});
// ✅ HyperExpress Style 1: Async (RECOMMENDED)
app.use(async (request, response) => {
console.log('Middleware');
// Automatically continues after resolve
});
// ✅ HyperExpress Style 2: Callback with next
app.use((request, response, next) => {
console.log('Middleware');
next(); // Must call next() to continue
});Execution Rules
| Pattern | Action | Result |
|---|---|---|
async (req, res) | No return | Continue to next middleware/handler |
async (req, res) | return response.xxx() | Stop execution, send response |
(req, res, next) | next() | Continue to next middleware/handler |
(req, res, next) | return response.xxx() | Stop execution, send response |
Examples
// ✅ Continue to next handler (async pattern - RECOMMENDED)
export default async (request: Request, response: Response) => {
request.startTime = Date.now();
// No return = continues automatically
}
// ✅ Stop execution and redirect (async pattern)
export default async (request: Request, response: Response) => {
if (!request.user) {
return response.redirect("/login"); // Stops here
}
// Continues if user exists
}
// ✅ Stop execution and send JSON (async pattern)
export default async (request: Request, response: Response) => {
if (!request.headers.authorization) {
return response.status(401).json({ error: "Unauthorized" });
}
// Continues if authorized
}Middleware Execution Flow
Route Definition
// Single middleware
Route.get("/profile", [Auth], ProfileController.show);
// Multiple middleware (executed in order)
Route.post("/upload", [Auth, uploadRateLimit, validateFile], UploadController.store);Execution Order
Route.post("/posts", [middleware1, middleware2, middleware3], Controller.store);
// Flow:
// 1. middleware1 executes
// - If returns response → STOP
// - If no return → continue to #2
// 2. middleware2 executes
// - If returns response → STOP
// - If no return → continue to #3
// 3. middleware3 executes
// - If returns response → STOP
// - If no return → continue to Controller.store
// 4. Controller.store executesBuilt-in Middleware
1. Auth Middleware
Protects routes requiring authentication.
import Auth from "app/middlewares/auth";
// Protect single route
Route.get("/dashboard", [Auth], DashboardController.index);
// Protect multiple routes
Route.get("/profile", [Auth], ProfileController.show);
Route.post("/profile", [Auth], ProfileController.update);2. CSRF Middleware
Protects against Cross-Site Request Forgery attacks.
import { csrf } from "app/middlewares/csrf";
// Apply globally in server.ts
webserver.use(csrf({
excludeAPIs: true, // Exclude /api/* routes (default: true)
excludePaths: ['/webhooks/*'] // Additional paths to exclude
}));3. Rate Limit Middleware
Prevents abuse by limiting requests per time window.
import {
authRateLimit, // 5 requests per 15 min
apiRateLimit, // 100 requests per 15 min
generalRateLimit, // 1000 requests per 15 min
passwordResetRateLimit, // 3 requests per hour
emailRateLimit, // 10 requests per hour
uploadRateLimit, // 50 requests per hour
createAccountRateLimit // 3 requests per hour
} from "app/middlewares/rateLimit";
// Usage
Route.post("/login", [authRateLimit], AuthController.processLogin);
Route.post("/register", [createAccountRateLimit], AuthController.processRegister);
Route.post("/api/upload", [Auth, uploadRateLimit], UploadController.store);Custom Rate Limiter
import { rateLimit } from "app/middlewares/rateLimit";
const customLimit = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
maxRequests: 100, // 100 requests
message: "Too many requests",
statusCode: 429,
keyGenerator: (request) => request.ip, // Rate limit by IP
skip: (request) => request.user?.is_admin // Skip for admins
});
Route.post("/api/data", [customLimit], DataController.store);4. Security Headers Middleware
Adds security-related HTTP headers to all responses.
import { securityHeaders } from "app/middlewares/securityHeaders";
// Apply globally in server.ts
webserver.use(securityHeaders());5. Inertia Middleware
Handles Inertia.js responses for SPA-like experience.
import inertia from "app/middlewares/inertia";
// Apply globally in server.ts
webserver.use(inertia());
// Now you can use response.inertia() in controllers
public async index(request: Request, response: Response) {
const posts = await DB.selectFrom("posts").selectAll().execute();
return response.inertia("posts/index", { posts });
}Creating Custom Middleware
Basic Middleware Template
// app/middlewares/myMiddleware.ts
import { Request, Response } from "../../type";
export default async (request: Request, response: Response) => {
// Your logic here
// Option 1: Continue to next handler (no return)
request.customData = "some value";
// Option 2: Stop and send response
// return response.status(400).json({ error: "Bad request" });
}Example 1: Request Logger
// app/middlewares/requestLogger.ts
import { Request, Response } from "../../type";
import { logInfo } from "../services/Logger";
export default async (request: Request, response: Response) => {
const startTime = Date.now();
logInfo("Request received", {
method: request.method,
url: request.url,
ip: request.ip,
userAgent: request.headers['user-agent']
});
// Continue to next handler
request.startTime = startTime;
}Usage:
import requestLogger from "../app/middlewares/requestLogger";
// Apply to specific routes
Route.get("/api/data", [requestLogger], DataController.index);
// Or globally
webserver.use(requestLogger);Example 2: API Key Validation
// app/middlewares/apiKey.ts
import { Request, Response } from "../../type";
export default async (request: Request, response: Response) => {
const apiKey = request.headers['x-api-key'] as string;
if (!apiKey) {
return response.status(401).json({
error: "API key is required",
code: "MISSING_API_KEY"
});
}
if (apiKey !== process.env.API_KEY) {
return response.status(403).json({
error: "Invalid API key",
code: "INVALID_API_KEY"
});
}
// Valid API key, continue
}Example 3: Role-Based Access Control
// app/middlewares/requireAdmin.ts
import { Request, Response } from "../../type";
export default async (request: Request, response: Response) => {
// Assumes Auth middleware ran first
if (!request.user) {
return response.status(401).json({ error: "Unauthorized" });
}
if (!request.user.is_admin) {
return response.status(403).json({
error: "Admin access required"
});
}
// User is admin, continue
}Usage:
import Auth from "../app/middlewares/auth";
import requireAdmin from "../app/middlewares/requireAdmin";
// Both middleware must pass
Route.get("/admin/users", [Auth, requireAdmin], AdminController.users);
Route.delete("/admin/users/:id", [Auth, requireAdmin], AdminController.deleteUser);Example 4: CORS Middleware
// app/middlewares/cors.ts
import { Request, Response } from "../../type";
export default async (request: Request, response: Response) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['*'];
const origin = request.headers.origin as string;
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin || '*');
}
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
response.setHeader('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (request.method === 'OPTIONS') {
return response.status(204).send('');
}
// Continue for other methods
}Common Patterns
Pattern 1: Conditional Middleware
// app/middlewares/conditionalAuth.ts
import { Request, Response } from "../../type";
import SQLite from "../services/SQLite";
export default async (request: Request, response: Response) => {
// Only check auth for non-public routes
const publicRoutes = ['/login', '/register', '/'];
if (publicRoutes.includes(request.path)) {
return; // Skip auth check
}
// Check authentication for other routes
if (!request.cookies.auth_id) {
return response.redirect("/login");
}
const user = SQLite.get("SELECT * FROM users WHERE id = ?", [request.cookies.auth_id]);
if (!user) {
return response.redirect("/login");
}
request.user = user;
}Pattern 2: Middleware Factory
// app/middlewares/requireRole.ts
import { Request, Response } from "../../type";
export function requireRole(role: string) {
return async (request: Request, response: Response) => {
if (!request.user) {
return response.status(401).json({ error: "Unauthorized" });
}
if (request.user.role !== role) {
return response.status(403).json({
error: `${role} access required`
});
}
// User has required role
};
}
// Usage
import { requireRole } from "../app/middlewares/requireRole";
Route.get("/admin/dashboard", [Auth, requireRole('admin')], AdminController.dashboard);
Route.get("/moderator/reports", [Auth, requireRole('moderator')], ModeratorController.reports);Pattern 3: Async Data Loading
// app/middlewares/loadPost.ts
import { Request, Response } from "../../type";
import DB from "../services/DB";
export default async (request: Request, response: Response) => {
const { id } = request.params;
const post = await DB.selectFrom("posts")
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
if (!post) {
return response.status(404).json({ error: "Post not found" });
}
// Attach to request
request.post = post;
// Continue to handler
}
// Usage in controller
public async show(request: Request, response: Response) {
// Post already loaded by middleware
return response.json({ post: request.post });
}Error Handling
Try-Catch in Middleware
// app/middlewares/safeMiddleware.ts
import { Request, Response } from "../../type";
import { logError } from "../services/Logger";
export default async (request: Request, response: Response) => {
try {
// Your middleware logic
const data = await someAsyncOperation();
request.data = data;
} catch (error) {
logError("Middleware error", {
error: error.message,
stack: error.stack,
url: request.url
});
return response.status(500).json({
error: "Internal server error"
});
}
}Global Error Handler
// server.ts
webserver.set_error_handler((request, response, error) => {
logError("Unhandled error", {
error: error.message,
stack: error.stack,
url: request.url,
method: request.method
});
// Don't expose internal errors in production
const message = process.env.NODE_ENV === 'production'
? 'Internal server error'
: error.message;
return response.status(500).json({ error: message });
});Best Practices
✅ DO
1. Keep middleware focused
// ✅ Good - Single responsibility
export default async (request: Request, response: Response) => {
if (!request.user) {
return response.redirect("/login");
}
}2. Use descriptive names
// ✅ Good
import requireAdmin from "../app/middlewares/requireAdmin";
import validatePost from "../app/middlewares/validatePost";3. Order middleware correctly
// ✅ Good - Auth before authorization
Route.post("/admin/users", [Auth, requireAdmin], AdminController.createUser);
// ✅ Good - Validation before processing
Route.post("/posts", [Auth, validatePost, uploadRateLimit], PostController.store);4. Return response to stop execution
// ✅ Good
if (!isValid) {
return response.status(400).json({ error: "Invalid" });
}❌ DON'T
1. Don't use next()
// ❌ Bad - HyperExpress doesn't have next()
export default async (request, response, next) => {
console.log("Middleware");
next(); // ❌ This doesn't exist
}2. Don't forget to return when stopping
// ❌ Bad - Missing return
if (!request.user) {
response.redirect("/login"); // ❌ Will continue execution!
}
// ✅ Good
if (!request.user) {
return response.redirect("/login");
}Summary
Key Takeaways
- No
next()function - Return response to stop, no return to continue - Order matters - Middleware executes in array order
- Return to stop - Always return when sending response
- Keep focused - One responsibility per middleware
- Use built-in - Auth, CSRF, rate limiting, Inertia, security headers already available
Quick Reference
// Continue to next handler
export default async (request: Request, response: Response) => {
request.data = "value";
// No return
}
// Stop and send response
export default async (request: Request, response: Response) => {
return response.json({ error: "Error" });
}
// Conditional execution
export default async (request: Request, response: Response) => {
if (condition) {
return response.redirect("/login");
}
// Continues if condition is false
}