Custom Middleware
SheetBot supports loading custom Express middleware from external directories via the SHEETBOT_MIDDLEWARE_SEARCH_PATHS environment variable.
Usage
Set the environment variable to a colon-separated list of directories:
export SHEETBOT_MIDDLEWARE_SEARCH_PATHS="/path/to/middleware:/another/path"
SheetBot will search these directories for .ts files and load any that export setupMiddleware and/or setupPostRouteMiddleware functions.
Middleware Types
Pre-Route Middleware
Runs before route handlers. Use for:
- Authentication/authorization
- Request logging
- CORS configuration
- Rate limiting
- Request validation
- Body parsing modifications
- Custom headers
Export a setupMiddleware(app) function:
export function setupMiddleware(app: any) {
// Add middleware that runs before routes
app.use((req: any, res: any, next: any) => {
console.log("Custom pre-route middleware");
next();
});
}
Post-Route Middleware
Runs after route handlers. Use for:
- 404 handlers
- Error handlers
- Fallback responses
Export a setupPostRouteMiddleware(app) function:
export function setupPostRouteMiddleware(app: any) {
// Add middleware that runs after routes
app.use((req: any, res: any, next: any) => {
res.status(404).json({ error: "Not found" });
});
}
Both Types
A single file can export both functions:
export function setupMiddleware(app: any) {
// Pre-route middleware
app.use((req: any, res: any, next: any) => {
req.customData = "something";
next();
});
}
export function setupPostRouteMiddleware(app: any) {
// Post-route middleware
app.use((req: any, res: any) => {
res.status(404).json({ error: "Route not found" });
});
}
Examples
CORS Middleware
export function setupMiddleware(app: any) {
app.use((req: any, res: any, next: any) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") {
return res.sendStatus(200);
}
next();
});
}
Rate Limiting
const rateLimits = new Map<string, { count: number; resetAt: number }>();
export function setupMiddleware(app: any) {
app.use((req: any, res: any, next: any) => {
const ip = req.ip || "unknown";
const now = Date.now();
const limit = rateLimits.get(ip);
if (!limit || now > limit.resetAt) {
rateLimits.set(ip, { count: 1, resetAt: now + 60000 }); // 1 minute window
return next();
}
if (limit.count >= 100) {
return res.status(429).json({ error: "Rate limit exceeded" });
}
limit.count++;
next();
});
}
Custom Authentication
export function setupMiddleware(app: any) {
app.use((req: any, res: any, next: any) => {
const apiKey = req.headers["x-api-key"];
if (req.path.startsWith("/api/") && !apiKey) {
return res.status(401).json({ error: "API key required" });
}
// Validate API key
if (apiKey && !isValidApiKey(apiKey)) {
return res.status(403).json({ error: "Invalid API key" });
}
next();
});
}
function isValidApiKey(key: string): boolean {
// Your validation logic
return key === Deno.env.get("VALID_API_KEY");
}
Custom 404 Handler
export function setupPostRouteMiddleware(app: any) {
app.use((req: any, res: any) => {
res.status(404).json({
error: "Not Found",
path: req.path,
method: req.method,
timestamp: new Date().toISOString()
});
});
}
Error Handler
export function setupPostRouteMiddleware(app: any) {
app.use((err: any, req: any, res: any, next: any) => {
console.error("Error:", err);
res.status(err.status || 500).json({
error: err.message || "Internal Server Error",
timestamp: new Date().toISOString()
});
});
}
Loading Order
- SheetBot’s built-in middleware (logging, body parsing, static files)
- Custom pre-route middleware from
SHEETBOT_MIDDLEWARE_SEARCH_PATHS(alphabetically by filename) - SheetBot’s route handlers
- Custom post-route middleware from
SHEETBOT_MIDDLEWARE_SEARCH_PATHS(alphabetically by filename)
Within each directory, middleware files are loaded in alphabetical order.
Error Handling
- If a middleware directory doesn’t exist or isn’t accessible, it’s skipped with a warning
- If a middleware file fails to load or execute, an error is logged but other middleware continues loading
- The server will start even if all middleware fails to load
Access to SheetBot Components
Middleware has full access to the Express app instance. To access SheetBot-specific components (database, event emitters, etc.), you can:
- Import them directly from SheetBot’s modules
- Store references in
app.localsfrom an init script - Use environment variables for configuration
Example using init script:
// In an init script
import { openDatabase } from "./lib/db.ts";
export default function() {
// Store DB reference for middleware to use
globalThis.sheetbotDb = openDatabase();
}
// In middleware
export function setupMiddleware(app: any) {
const db = (globalThis as any).sheetbotDb;
app.use((req: any, res: any, next: any) => {
// Use db here
next();
});
}