Routing and Middleware Documentation
Complete guide to routing and middleware in SonicJS using the Hono framework, optimized for Cloudflare Workers.
Overview
SonicJS uses Hono, a lightweight web framework optimized for Cloudflare Workers. The middleware pipeline processes every request through a series of layers before reaching route handlers.
Key Concepts
- Middleware - Functions that execute before route handlers to process requests
- Middleware Ordering - Order matters - middleware executes in the sequence it's registered
- Route Protection - Middleware can protect routes based on authentication, roles, and permissions
- Context Object - Middleware can set values on the context (
c.set()) for downstream use
Authentication
JWT-based auth with KV caching
Authorization
Fine-grained permission system
Bootstrap
System initialization on startup
Logging
Request/response/security logging
Performance
Caching and security headers
Plugin Middleware
Route protection by plugin status
Middleware Pipeline
The middleware pipeline executes in the following order for every request:
Middleware Configuration
// File: /src/index.ts
// 1. Bootstrap - System initialization (runs once per worker)
app.use('*', bootstrapMiddleware())
// 2. Logging - Request/response logging
app.use('*', loggingMiddleware())
app.use('*', securityLoggingMiddleware())
app.use('*', performanceLoggingMiddleware(1000)) // Log slow requests
// 3. Standard Middleware
app.use('*', logger()) // Hono's built-in logger
app.use('*', cors()) // CORS headers
app.use('*', securityHeaders()) // Security headers
app.use('/api/*', prettyJSON()) // JSON formatting for API routes
// 4. Route-specific middleware (applied to specific paths)
app.use('/admin/*', requireAuth())
app.use('/admin/*', requireRole(['admin', 'editor']))
app.use('/admin/*', cacheHeaders(60))
Middleware Execution Order
Request
↓
Bootstrap Middleware → Check/run system initialization
↓
Logging Middleware → Log request start
↓
Security Logging → Check for suspicious patterns
↓
Performance Logging → Track request timing
↓
Standard Middleware → CORS, security headers, etc.
↓
Route-specific Middleware → Auth, roles, permissions
↓
Route Handler → Execute business logic
↓
Logging Middleware → Log request completion
↓
Response
Authentication Middleware
The authentication system uses JWT tokens stored in HTTP-only cookies.
Authentication Manager
Auth Manager
import { AuthManager } from '../middleware/auth'
// Generate JWT token
const token = await AuthManager.generateToken(
userId,
email,
role
)
// Verify JWT token
const payload = await AuthManager.verifyToken(token)
// Returns: { userId, email, role, exp, iat } or null
// Hash password
const hash = await AuthManager.hashPassword(password)
// Verify password
const isValid = await AuthManager.verifyPassword(password, hash)
requireAuth Middleware
Requires valid authentication to access a route.
Require Auth
import { requireAuth } from '../middleware/auth'
// Protect a route
app.get('/protected', requireAuth(), async (c) => {
const user = c.get('user')
// user contains: { userId, email, role, exp, iat }
return c.json({ message: 'Welcome!', user })
})
How it works:
- Checks for token in
Authorizationheader (Bearer token) - Falls back to
auth_tokencookie if no header present - Verifies token with KV cache (5-minute TTL)
- Falls back to JWT verification if not cached
- Sets
userobject on context for downstream use - Returns 401 error or redirects to login if invalid
requireRole Middleware
Requires specific role(s) to access a route.
Role-Based Access
import { requireRole } from '../middleware/auth'
// Single role
app.get('/admin-only',
requireAuth(),
requireRole('admin'),
async (c) => {
return c.json({ message: 'Admin area' })
}
)
// Multiple roles (any of them)
app.get('/content-edit',
requireAuth(),
requireRole(['admin', 'editor']),
async (c) => {
return c.json({ message: 'Content editing area' })
}
)
Role Hierarchy:
admin- Full system accesseditor- Content management and publishingviewer- Read-only access
optionalAuth Middleware
Allows authenticated and unauthenticated access, but populates user if authenticated.
Optional Auth
import { optionalAuth } from '../middleware/auth'
// Public API with optional auth
app.get('/content', optionalAuth(), async (c) => {
const user = c.get('user')
const db = c.env.DB
// Show different content based on authentication
const query = user
? 'SELECT * FROM content WHERE status IN (?, ?, ?)'
: 'SELECT * FROM content WHERE status = ?'
const params = user
? ['published', 'draft', 'scheduled']
: ['published']
const { results } = await db.prepare(query).bind(...params).all()
return c.json({ data: results, authenticated: !!user })
})
Authorization and Permissions
Fine-grained permission system for controlling access to specific resources.
Permission Manager
Permission Manager
import { PermissionManager } from '../middleware/permissions'
// Get user permissions
const permissions = await PermissionManager.getUserPermissions(db, userId)
// Returns: { userId, role, permissions: string[], teamPermissions: {} }
// Check single permission
const hasPermission = await PermissionManager.hasPermission(
db,
userId,
'content.edit'
)
// Check multiple permissions
const permissionMap = await PermissionManager.checkMultiplePermissions(
db,
userId,
['content.edit', 'content.publish', 'users.create']
)
// Returns: { 'content.edit': true, 'content.publish': false, ... }
// Clear user permission cache
PermissionManager.clearUserCache(userId)
// Clear all permission cache
PermissionManager.clearAllCache()
requirePermission Middleware
Requires specific permission to access a route.
Permission-Based Access
import { requirePermission } from '../middleware/permissions'
// Single permission
app.post('/content',
requireAuth(),
requirePermission('content.create'),
async (c) => {
// User has content.create permission
return c.json({ message: 'Content created' })
}
)
// Permission with team context
app.get('/teams/:teamId/settings',
requireAuth(),
requirePermission('team.settings', 'teamId'),
async (c) => {
const teamId = c.req.param('teamId')
// User has team.settings permission for this specific team
return c.json({ message: `Team ${teamId} settings` })
}
)
Permission Naming Convention
Permissions follow the pattern: resource.action
Common Permissions:
content.create- Create contentcontent.edit- Edit contentcontent.publish- Publish contentcontent.delete- Delete contentusers.create- Create usersusers.edit- Edit usersusers.delete- Delete usersmedia.upload- Upload media filesmedia.delete- Delete media files
Bootstrap Middleware
Handles system initialization on worker startup.
Purpose
The bootstrap middleware runs once per Cloudflare Worker instance to:
- Run pending database migrations
- Sync collection configurations
- Bootstrap core plugins
- Install demo plugins (development only)
Bootstrap Middleware
import { bootstrapMiddleware, resetBootstrap } from '../middleware/bootstrap'
// Apply to all routes
app.use('*', bootstrapMiddleware())
// Reset bootstrap flag (useful for testing)
resetBootstrap()
Implementation
Bootstrap Process
let bootstrapComplete = false
export function bootstrapMiddleware() {
return async (c: Context, next: Next) => {
// Skip if already bootstrapped
if (bootstrapComplete) {
return next()
}
// Skip bootstrap for static assets
const path = c.req.path
if (
path.startsWith('/images/') ||
path.startsWith('/assets/') ||
path === '/health' ||
path.endsWith('.js') ||
path.endsWith('.css')
) {
return next()
}
try {
console.log('[Bootstrap] Starting system initialization...')
// 1. Run database migrations
const migrationService = new MigrationService(c.env.DB)
await migrationService.runPendingMigrations()
// 2. Sync collection configurations
await syncCollections(c.env.DB)
// 3. Bootstrap core plugins
const bootstrapService = new PluginBootstrapService(c.env.DB)
const needsBootstrap = await bootstrapService.isBootstrapNeeded()
if (needsBootstrap) {
await bootstrapService.bootstrapCorePlugins()
await bootstrapService.installDemoPlugins()
}
bootstrapComplete = true
console.log('[Bootstrap] System initialization completed')
} catch (error) {
console.error('[Bootstrap] Error during initialization:', error)
// Continue even if bootstrap fails
}
return next()
}
}
Logging Middleware
Comprehensive request/response logging with security monitoring.
Standard Logging Middleware
Logs all HTTP requests and responses.
Logging
import { loggingMiddleware } from '../middleware/logging'
app.use('*', loggingMiddleware())
What it logs:
- Request method, URL, and headers
- Response status code
- Request duration
- User ID (if authenticated)
- IP address and user agent
- Request ID (generated UUID)
Security Logging Middleware
Monitors suspicious activity and security events.
Security Logging
import { securityLoggingMiddleware } from '../middleware/logging'
app.use('*', securityLoggingMiddleware())
What it monitors:
- Suspicious request patterns (SQL injection, XSS attempts)
- Authentication failures
- Admin area access
- Unauthorized access attempts
Suspicious patterns detected:
const suspiciousPatterns = [
/script[^>]*>/i, // XSS attempts
/javascript:/i, // JavaScript protocol
/on\w+\s*=/i, // Event handlers
/\.\.\/\.\.\//, // Directory traversal
/\/etc\/passwd/i, // System file access
/union\s+select/i, // SQL injection
/drop\s+table/i // SQL injection
]
Performance Logging Middleware
Tracks slow requests for performance monitoring.
Performance Logging
import { performanceLoggingMiddleware } from '../middleware/logging'
// Log requests slower than 1000ms (1 second)
app.use('*', performanceLoggingMiddleware(1000))
// Log requests slower than 500ms for critical API
app.use('/api/critical/*', performanceLoggingMiddleware(500))
Performance Middleware
Middleware for caching, compression, and security headers.
Cache Headers Middleware
Adds cache-control headers to responses.
Cache Headers
import { cacheHeaders } from '../middleware/performance'
// Cache for 60 seconds
app.use('/admin/*', cacheHeaders(60))
// Cache for 5 minutes
app.use('/api/static/*', cacheHeaders(300))
How it works:
- Only caches successful HTML responses (200 status)
- Sets
Cache-Control: private, max-age={maxAge} - Private caching prevents CDN caching of authenticated pages
Security Headers Middleware
Adds security headers to all responses.
Security Headers
import { securityHeaders } from '../middleware/performance'
app.use('*', securityHeaders())
Headers added:
'X-Content-Type-Options': 'nosniff'
'X-Frame-Options': 'SAMEORIGIN'
'X-XSS-Protection': '1; mode=block'
Plugin Middleware
Controls access to plugin routes based on plugin activation status.
requireActivePlugin Middleware
Ensures a plugin is active before allowing access to its routes.
Plugin Middleware
import { requireActivePlugin } from '../middleware/plugin-middleware'
// Protect FAQ plugin routes
app.use('/admin/faq/*', requireActivePlugin('faq'))
app.route('/admin/faq', adminFAQRoutes)
// Protect workflow plugin routes
app.use('/admin/workflow/*', requireActivePlugin('workflow'))
app.route('/admin/workflow', createWorkflowAdminRoutes())
// Protect cache plugin routes
app.use('/admin/cache/*', requireActivePlugin('cache'))
app.route('/admin/cache', cacheRoutes)
How it works:
- Queries database for plugin status
- Returns 404 with user-friendly message if plugin is not active
- Allows request to continue if plugin is active
- Fails open (allows access) if database query fails
Route Structure
SonicJS organizes routes into logical modules.
Public Routes
No authentication required.
Public Routes
// Authentication pages
GET /auth/login # Login page
POST /auth/login # Login form submission
GET /auth/register # Registration page
POST /auth/register # Registration form submission
GET /auth/logout # Logout
POST /auth/logout # Logout API
// Public API
GET /api/ # OpenAPI specification
GET /api/health # Health check
GET /api/collections # List collections
GET /api/content # List content (published only)
// Documentation
GET /docs # Documentation home
// Static files
GET /images/* # Serve images
GET /media/serve/:key # Serve media files
// Health check
GET /health # System health
Admin Routes
Requires authentication + admin or editor role.
Admin Routes
// Dashboard
GET /admin/ # Admin dashboard
GET /admin/api/stats # Dashboard statistics (HTMX)
GET /admin/api/system-status # System status (HTMX)
// Collections
GET /admin/collections # List collections
GET /admin/collections/new # New collection form
POST /admin/collections # Create collection
GET /admin/collections/:id # Edit collection
PUT /admin/collections/:id # Update collection
DELETE /admin/collections/:id # Delete collection
// Content
GET /admin/content/ # List content
GET /admin/content/new # New content form
POST /admin/content/ # Create content
GET /admin/content/:id/edit # Edit content form
PUT /admin/content/:id # Update content
DELETE /admin/content/:id # Delete content
// Media
GET /admin/media/ # Media library
GET /admin/media/search # Search media (HTMX)
GET /admin/media/:id/details # File details (HTMX)
POST /admin/media/upload # Upload files
PUT /admin/media/:id # Update metadata
DELETE /admin/media/:id # Delete file
// Users
GET /admin/users # List users
POST /admin/users/:id/toggle # Toggle user status
GET /admin/users/export # Export users CSV
// Plugins
GET /admin/plugins # List plugins
POST /admin/plugins/:id/toggle # Toggle plugin status
GET /admin/plugins/:id # Plugin details
// Settings
GET /admin/settings # Settings page
GET /admin/settings/:tab # Settings tab
POST /admin/settings # Save settings
Creating Custom Middleware
Basic Middleware Structure
Middleware Template
import { Context, Next } from 'hono'
type Bindings = {
DB: D1Database
KV: KVNamespace
}
type Variables = {
user?: {
userId: string
email: string
role: string
}
customData?: any
}
export function customMiddleware() {
return async (c: Context<{ Bindings: Bindings; Variables: Variables }>, next: Next) => {
// 1. Pre-processing (before route handler)
console.log('Before route handler')
// 2. Set context variables
c.set('customData', { foo: 'bar' })
// 3. Call next middleware/handler
await next()
// 4. Post-processing (after route handler)
console.log('After route handler')
}
}
Example: Rate Limiting Middleware
Rate Limiter
export function rateLimitMiddleware(maxRequests: number, windowMs: number) {
const requests = new Map<string, { count: number; resetTime: number }>()
return async (c: Context, next: Next) => {
const clientId = c.req.header('cf-connecting-ip') || 'unknown'
const now = Date.now()
// Get or create rate limit record
let record = requests.get(clientId)
if (!record || now > record.resetTime) {
record = { count: 0, resetTime: now + windowMs }
requests.set(clientId, record)
}
// Check rate limit
if (record.count >= maxRequests) {
const retryAfter = Math.ceil((record.resetTime - now) / 1000)
c.header('Retry-After', retryAfter.toString())
return c.json({ error: 'Rate limit exceeded' }, 429)
}
// Increment counter
record.count++
// Add rate limit headers
c.header('X-RateLimit-Limit', maxRequests.toString())
c.header('X-RateLimit-Remaining', (maxRequests - record.count).toString())
c.header('X-RateLimit-Reset', record.resetTime.toString())
await next()
}
}
// Usage
app.use('/api/*', rateLimitMiddleware(100, 60000)) // 100 requests per minute
Middleware Best Practices
- Keep middleware focused on a single responsibility
- Minimize database queries in middleware
- Use context variables to pass data between middleware
- Handle errors gracefully and provide meaningful error messages
- Consider performance impact of middleware execution order
- Test middleware independently before integrating