This document provides comprehensive documentation for the API middleware examples demonstrating authentication, rate limiting, and security features in the Nuxt 4 application.
- Overview
- Architecture
- Middleware Execution Order
- Examples
- API Config Reference
- Error Handling
- Best Practices
This application uses a comprehensive middleware system for API routes that provides:
- Authentication via JWT tokens with Logto
- Authorization via role-based access control
- Rate Limiting to prevent abuse
- Security Headers for protection against common vulnerabilities
- Structured Logging for debugging and monitoring
The server has two middleware files that execute in order:
01-api-auth.ts- Runs first, sets up context and applies security02-api-protected.ts- Runs second, validates JWT tokens
server/utils/api-middleware-config.ts - Centralized configuration for:
- Public routes
- Protected routes
- Rate limiting rules
- Security headers
Request → 01-api-auth.ts → 02-api-protected.ts → API Route
↓ ↓
- Add headers - Extract JWT
- Rate limit - Validate token
- Init Logto - Set user context
↓ ↓
Headers sent User available
to all routes in event.context
- Middleware files execute in alphabetical order by filename
01-api-auth.tsalways runs first (prepares context)02-api-protected.tsruns second (validates auth)- API routes execute last
Endpoint: GET /api/example/health
Purpose: Public health check endpoint to verify API status.
Authentication: None required
Rate Limiting: 100 requests per 15 minutes (default)
curl http://localhost:3000/api/example/health{
"status": "ok",
"timestamp": "2024-01-15T10:30:00.000Z",
"uptime": 3600.5,
"environment": "development",
"version": "1.0.0"
}export default defineEventHandler(async (event) => {
// Mark as public - api-auth adds headers only
event.context.apiPublic = true
// Return health status
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0'
}
})X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originContent-Security-Policy: default-src 'self'
Endpoint: GET /api/example/public
Purpose: Returns publicly accessible information without authentication.
Authentication: None required
Rate Limiting: 100 requests per 15 minutes (default)
curl http://localhost:3000/api/example/public{
"message": "This is public data",
"accessibleBy": "everyone",
"features": [
"No authentication required",
"Rate limiting applied (100 req/15 min)",
"Security headers included"
],
"timestamp": "2024-01-15T10:30:00.000Z"
}export default defineEventHandler(async (event) => {
// Mark as public route
event.context.apiPublic = true
return {
message: 'This is public data',
accessibleBy: 'everyone',
features: [
'No authentication required',
'Rate limiting applied (100 req/15 min)',
'Security headers included'
],
timestamp: new Date().toISOString()
}
})Endpoint: GET /api/example/user/profile
Purpose: Returns authenticated user's profile data.
Authentication: Required (JWT Bearer token)
Rate Limiting: 100 requests per 15 minutes (default)
curl -H "Authorization: Bearer <jwt_token>" \
http://localhost:3000/api/example/user/profile{
"userId": "user-123",
"email": "user@example.com",
"name": "John Doe",
"picture": "https://example.com/avatar.jpg",
"emailVerified": true,
"iss": "https://your-logto-app.com",
"aud": "your-client-id",
"exp": 1705330200,
"iat": 1705326600,
"features": [
"JWT token validated",
"User authenticated",
"Rate limiting applied (100 req/15 min)",
"Security headers included"
],
"timestamp": "2024-01-15T10:30:00.000Z"
}{
"statusCode": 401,
"statusMessage": "Unauthorized",
"message": "No token provided or token is invalid"
}export default defineEventHandler(async (event) => {
// Mark as protected - api-protected validates JWT
event.context.apiProtected = true
// User is validated by middleware
const user = event.context.user
return {
userId: user.sub,
email: user.email,
name: user.name || 'User',
picture: user.picture || null,
emailVerified: user.email_verified || false,
// JWT claims
iss: user.iss,
aud: user.aud,
exp: user.exp,
iat: user.iat
}
})Endpoint: GET /api/example/admin/users
Purpose: Lists all users (admin only).
Authentication: Required (JWT Bearer token with admin role)
Rate Limiting: 50 requests per 15 minutes (admin-specific)
curl -H "Authorization: Bearer <admin_jwt_token>" \
http://localhost:3000/api/example/admin/users{
"admin": {
"userId": "admin-123",
"email": "admin@example.com",
"name": "Admin User"
},
"users": [
{
"id": "user-1",
"email": "user1@example.com",
"name": "User One",
"role": "user"
},
{
"id": "user-2",
"email": "user2@example.com",
"name": "User Two",
"role": "user"
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 2
},
"features": [
"JWT token validated",
"Admin role verified",
"Rate limiting applied (50 req/15 min for admin)",
"Security headers included"
],
"timestamp": "2024-01-15T10:30:00.000Z"
}{
"statusCode": 403,
"statusMessage": "Forbidden: You do not have the required permissions",
"data": {
"error": "insufficient_permissions",
"message": "This endpoint requires admin role",
"userRoles": ["user"],
"requiredRoles": ["admin"]
}
}export default defineEventHandler(async (event) => {
event.context.apiProtected = true
const user = event.context.user
// Check user roles
const userRoles = (user.roles as string[]) || []
const requiredRoles = ['admin']
const hasRequiredRole = requiredRoles.some(
role => userRoles.includes(role)
)
if (!hasRequiredRole) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
data: {
error: 'insufficient_permissions',
message: 'This endpoint requires admin role',
userRoles,
requiredRoles
}
})
}
// Return users list
return { users: [...] }
})Endpoint: GET /api/example/custom
Purpose: Demonstrates dynamic configuration using API_CONFIG utilities.
Authentication: None required (public via config)
Rate Limiting: 100 requests per 15 minutes (default)
curl http://localhost:3000/api/example/custom{
"route": "/api/example/custom",
"isPublic": true,
"isProtected": false,
"rateLimitConfig": {
"maxRequests": 100,
"windowSeconds": 900,
"requestsPerMinute": 7
},
"securityHeaders": {
"count": 5,
"headers": [
"X-Content-Type-Options",
"X-Frame-Options",
"X-XSS-Protection",
"Referrer-Policy",
"Content-Security-Policy"
]
},
"features": [
"Dynamic public route detection via isPublicRoute()",
"Dynamic protected route detection via isProtectedRoute()",
"Route-specific rate limits via getRateLimitConfig()",
"Security headers via getSecurityHeaders()",
"Configured in API_CONFIG (api-middleware-config.ts)"
],
"timestamp": "2024-01-15T10:30:00.000Z"
}import {
isPublicRoute,
isProtectedRoute,
getRateLimitConfig,
getSecurityHeaders
} from '../../utils/api-middleware-config'
export default defineEventHandler(async (event) => {
const route = event.path
// Dynamic route detection
if (isPublicRoute(route)) {
event.context.apiPublic = true
}
if (isProtectedRoute(route)) {
event.context.apiProtected = true
}
// Get route-specific config
const rateLimitConfig = getRateLimitConfig(route)
const securityHeaders = getSecurityHeaders()
return {
route,
isPublic: isPublicRoute(route),
isProtected: isProtectedRoute(route),
rateLimitConfig: {
maxRequests: rateLimitConfig.maxRequests,
windowSeconds: rateLimitConfig.windowSeconds
},
securityHeaders: {
count: Object.keys(securityHeaders).length
}
}
})Endpoint: POST /api/example/auth/login
Purpose: Public authentication endpoint with strict rate limiting.
Authentication: None required
Rate Limiting: 10 requests per 1 minute (auth-specific)
curl -X POST http://localhost:3000/api/example/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password123"}'{
"success": true,
"message": "Login successful",
"user": {
"email": "user@example.com",
"name": "User"
},
"rateLimitInfo": {
"maxRequests": 10,
"windowSeconds": 60,
"note": "Rate limit headers are included in response"
},
"features": [
"Public endpoint",
"Strict rate limiting (10 req/1 min for auth routes)",
"Security headers included",
"Rate limit headers in response"
],
"timestamp": "2024-01-15T10:30:00.000Z"
}{
"statusCode": 400,
"statusMessage": "Bad Request: Email and password are required",
"data": {
"error": "missing_credentials",
"message": "Email and password are required"
}
}{
"statusCode": 401,
"statusMessage": "Unauthorized: Invalid email or password",
"data": {
"error": "invalid_credentials",
"message": "Invalid email or password"
}
}HTTP/1.1 200 OK
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1705330200import { getRateLimitConfig } from '../../utils/api-middleware-config'
export default defineEventHandler(async (event) => {
event.context.apiPublic = true
const body = await readBody(event)
const { email, password } = body
// Get auth-specific rate limit
const rateLimitConfig = getRateLimitConfig('/api/auth/login')
// Validate credentials
if (!email || !password) {
throw createError({
statusCode: 400,
data: {
error: 'missing_credentials'
}
})
}
// Mock validation
const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
const isValidPassword = password.length >= 6
if (!isValidEmail || !isValidPassword) {
throw createError({
statusCode: 401,
data: {
error: 'invalid_credentials'
}
})
}
return { success: true, user: { email } }
})Endpoint: GET /api/example/rate-limited
Purpose: Demonstrates rate limiting headers and security headers.
Authentication: Required (JWT Bearer token)
Rate Limiting: 100 requests per 15 minutes (default)
curl -H "Authorization: Bearer <jwt_token>" \
http://localhost:3000/api/example/rate-limited{
"user": {
"userId": "user-123",
"email": "user@example.com"
},
"rateLimit": {
"limit": 100,
"remaining": 95,
"reset": 1705330200,
"resetDate": "2024-01-15T10:30:00.000Z",
"note": "These headers are automatically added by api-auth middleware"
},
"securityHeaders": {
"applied": true,
"headers": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Security-Policy": "default-src 'self'"
},
"note": "Security headers are automatically added by api-auth middleware"
},
"features": [
"JWT token validated",
"Rate limiting applied (100 req/15 min default)",
"Rate limit headers in response (X-RateLimit-*)",
"Security headers in response",
"User authenticated"
],
"timestamp": "2024-01-15T10:30:00.000Z"
}HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705330200
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'export default defineEventHandler(async (event) => {
event.context.apiProtected = true
const user = event.context.user
// Get rate limit headers from response
const rateLimitHeaders = {
limit: event.node.res.getHeader('X-RateLimit-Limit'),
remaining: event.node.res.getHeader('X-RateLimit-Remaining'),
reset: event.node.res.getHeader('X-RateLimit-Reset')
}
// Get security headers
const securityHeaders = {
contentTypeOptions: event.node.res.getHeader('X-Content-Type-Options'),
frameOptions: event.node.res.getHeader('X-Frame-Options'),
xssProtection: event.node.res.getHeader('X-XSS-Protection'),
referrerPolicy: event.node.res.getHeader('Referrer-Policy'),
csp: event.node.res.getHeader('Content-Security-Policy')
}
return {
user: { userId: user.sub },
rateLimit: rateLimitHeaders,
securityHeaders
}
})Routes that don't require authentication:
API_CONFIG.publicPaths = [
'/api/health',
'/api/public',
'/api/auth',
'/api/auth/sign-in',
'/api/auth/sign-out'
]Routes that require authentication:
API_CONFIG.protectedPaths = [
'/api/user',
'/api/user/*',
'/api/admin',
'/api/admin/*',
'/api/dashboard',
'/api/dashboard/*'
]Default rate limits:
API_CONFIG.rateLimit = {
maxRequests: 100, // 100 requests
windowSeconds: 900, // per 15 minutes
routeLimits: {
'/api/auth/*': {
maxRequests: 10, // 10 requests
windowSeconds: 60 // per 1 minute
},
'/api/admin/*': {
maxRequests: 50, // 50 requests
windowSeconds: 900 // per 15 minutes
}
}
}Default security headers:
API_CONFIG.security = {
csp: "default-src 'self'",
frameOptions: 'DENY',
nosniff: 'nosniff',
xssProtection: '1; mode=block',
referrerPolicy: 'strict-origin-when-cross-origin'
}| Status Code | Description | Example Scenario |
|---|---|---|
| 200 | OK | Successful request |
| 400 | Bad Request | Missing required parameters |
| 401 | Unauthorized | Missing or invalid token |
| 403 | Forbidden | Insufficient permissions |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
All errors follow this format:
{
"statusCode": 401,
"statusMessage": "Unauthorized",
"message": "No token provided or token is invalid",
"data": {
"error": "auth_required",
"details": "..."
}
}HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705330200
{
"statusCode": 429,
"statusMessage": "Too Many Requests",
"message": "Rate limit exceeded",
"data": {
"error": "rate_limit_exceeded",
"retryAfter": 60
}
}export default defineEventHandler(async (event) => {
// Mark as public FIRST
event.context.apiPublic = true
// Rest of your code
})export default defineEventHandler(async (event) => {
// Mark as protected FIRST
event.context.apiProtected = true
// User is now available
const user = event.context.user
})export default defineEventHandler(async (event) => {
event.context.apiProtected = true
const user = event.context.user
if (!user) {
throw createError({
statusCode: 500,
statusMessage: 'User context not available'
})
}
// Use user data
const userId = user.sub
const email = user.email
})export default defineEventHandler(async (event) => {
event.context.apiProtected = true
const user = event.context.user
const userRoles = (user.roles as string[]) || []
const requiredRoles = ['admin', 'editor']
const hasRequiredRole = requiredRoles.some(
role => userRoles.includes(role)
)
if (!hasRequiredRole) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
data: {
error: 'insufficient_permissions',
userRoles,
requiredRoles
}
})
}
})import { getRateLimitConfig } from '#utils/api-middleware-config'
export default defineEventHandler(async (event) => {
const route = event.path
const limits = getRateLimitConfig(route)
console.log(`Max requests: ${limits.maxRequests}`)
console.log(`Window: ${limits.windowSeconds}s`)
})import { isPublicRoute, isProtectedRoute } from '#utils/api-middleware-config'
export default defineEventHandler(async (event) => {
const route = event.path
if (isPublicRoute(route)) {
event.context.apiPublic = true
} else if (isProtectedRoute(route)) {
event.context.apiProtected = true
}
})export default defineEventHandler(async (event) => {
const limit = event.node.res.getHeader('X-RateLimit-Limit')
const remaining = event.node.res.getHeader('X-RateLimit-Remaining')
const reset = event.node.res.getHeader('X-RateLimit-Reset')
return {
rateLimit: {
limit: parseInt(limit),
remaining: parseInt(remaining),
reset: new Date(parseInt(reset) * 1000).toISOString()
}
}
})curl http://localhost:3000/api/example/healthcurl http://localhost:3000/api/example/public# Get JWT token first from Logto
TOKEN="your_jwt_token"
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/example/user/profile# Use admin JWT token
ADMIN_TOKEN="your_admin_jwt_token"
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3000/api/example/admin/users# Make 11 requests (exceeds 10 req/min limit)
for i in {1..11}; do
curl -X POST http://localhost:3000/api/example/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password123"}'
echo ""
doneTOKEN="your_jwt_token"
curl -I -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/example/rate-limitedThis middleware system provides:
✅ Authentication - JWT validation with Logto ✅ Authorization - Role-based access control ✅ Rate Limiting - Configurable per-route limits ✅ Security Headers - Automatic protection ✅ Structured Logging - Debugging and monitoring ✅ Dynamic Configuration - Centralized API config ✅ Type Safety - Full TypeScript support ✅ Easy to Use - Simple flag-based approach
For more information, see:
- Middleware files in
server/middleware/ - Configuration in
server/utils/api-middleware-config.ts - Logger in
server/utils/logger.ts - Rate limiter in
server/utils/rate-limiter.ts