@verisure-italy/express-error-handler-middleware
v1.7.2
Published
Express error handler middleware with support for domain-specific errors
Maintainers
Readme
Error Handler Middleware
A comprehensive Express error handler middleware that provides standardized error responses for all domain-specific errors from various packages including router-middleware and authentication-middleware.
Features
- ✅ Standardized Error Responses - Consistent error format across all error types
- ✅ Domain Error Support - Built-in handling for validation, authentication, authorization, and not found errors
- ✅ Zod Validation Errors - Detailed field-by-field validation error messages
- ✅ Type-Safe - Full TypeScript support with detailed error types
- ✅ Customizable - Custom formatters, error handlers, and skip conditions
- ✅ Environment Aware - Different behavior for development and production
- ✅ Logging Support - Built-in error logging with custom handler support
- ✅ Stack Traces - Optional stack traces in development mode
Installation
pnpm add @verisure-italy/error-handler-middlewareQuick Start
Basic Usage
import express from 'express'
import { errorHandler } from '@verisure-italy/error-handler-middleware'
const app = express()
// ... your routes ...
// Add error handler as the last middleware
app.use(errorHandler())With Configuration
import { errorHandler } from '@verisure-italy/error-handler-middleware'
app.use(
errorHandler({
logErrors: true,
showDetails: process.env.NODE_ENV !== 'production',
includeStackTrace: process.env.NODE_ENV === 'development',
})
)Error Response Format
All errors follow a consistent structure:
interface ErrorResponse {
error: string // Error type (e.g., "Validation Error")
message: string // Human-readable message
statusCode: number // HTTP status code
code: string // Machine-readable error code
timestamp: string // ISO 8601 timestamp
path?: string // Request path where error occurred
details?: any // Additional error details (environment-dependent)
}Supported Error Types
1. Validation Errors (422)
From router-middleware with Zod:
// Request
POST /api/users
{
"username": "",
"roles": []
}
// Response
{
"error": "Validation Error",
"message": "The request contains invalid data",
"statusCode": 422,
"code": "VALIDATION_ERROR",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users",
"details": [
{
"field": "username",
"message": "String must contain at least 1 character(s)",
"code": "too_small",
"value": ""
},
{
"field": "roles",
"message": "Array must contain at least 1 element(s)",
"code": "too_small",
"value": []
}
]
}2. Authentication Errors (401)
From authentication-middleware:
// Response
{
"error": "Authentication Error",
"message": "Invalid token",
"statusCode": 401,
"code": "UNAUTHORIZED",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users"
}3. Authorization Errors (403)
From router-middleware ACL:
// Response
{
"error": "Authorization Error",
"message": "You do not have permission to access this resource",
"statusCode": 403,
"code": "FORBIDDEN",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users/123"
}4. Not Found Errors (404)
From router-middleware param resolver:
// Response
{
"error": "Not Found",
"message": "User not found",
"statusCode": 404,
"code": "NOT_FOUND",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users/unknown-id"
}5. Generic Errors (500)
Any unhandled error:
// Development Response (showDetails: true)
{
"error": "Internal Server Error",
"message": "Cannot read property 'id' of undefined",
"statusCode": 500,
"code": "INTERNAL_ERROR",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users",
"details": {
"name": "TypeError",
"stack": [
"TypeError: Cannot read property 'id' of undefined",
" at UserController.create (/app/controllers/user.js:15:20)",
" ..."
]
}
}
// Production Response (showDetails: false)
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"statusCode": 500,
"code": "INTERNAL_ERROR",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users"
}Configuration
ErrorHandlerConfig
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| includeStackTrace | boolean | NODE_ENV !== 'production' | Include stack trace in error response |
| logErrors | boolean | NODE_ENV === 'development' | Log errors to console |
| showDetails | boolean | NODE_ENV !== 'production' | Show detailed error information |
| onError | (error, req, res) => void \| Promise<void> | undefined | Custom error handler (for logging, monitoring, etc.) |
| skipError | (error, req) => boolean | () => false | Skip handling for specific errors |
| formatters | object | {} | Custom formatters for specific error types |
Advanced Usage
Custom Error Logging
import { errorHandler } from '@verisure-italy/error-handler-middleware'
import { logger } from './logger'
app.use(
errorHandler({
onError: async (error, req, res) => {
// Log to external service (e.g., Sentry, DataDog)
await logger.error('API Error', {
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
userId: req._auth?.user?.id,
statusCode: res.statusCode,
})
},
})
)Skip Specific Errors
app.use(
errorHandler({
skipError: (error, req) => {
// Skip 404 errors on static files
if (error.statusCode === 404 && req.path.startsWith('/static')) {
return true
}
// Skip specific error types
if (error.code === 'CUSTOM_ERROR') {
return true
}
return false
},
})
)Custom Error Formatters
app.use(
errorHandler({
formatters: {
validation: (error) => ({
error: 'Invalid Input',
message: 'Please check your input and try again',
statusCode: 422,
code: 'INVALID_INPUT',
timestamp: new Date().toISOString(),
details: {
fields: error.errors?.map(e => e.field) || [],
},
}),
authentication: (error) => ({
error: 'Auth Failed',
message: 'Please log in to continue',
statusCode: 401,
code: 'AUTH_REQUIRED',
timestamp: new Date().toISOString(),
}),
},
})
)Different Configs for Different Environments
const errorConfig = {
development: {
logErrors: true,
showDetails: true,
includeStackTrace: true,
},
production: {
logErrors: false,
showDetails: false,
includeStackTrace: false,
onError: async (error, req) => {
// Send to monitoring service
await sentry.captureException(error, {
extra: { path: req.path, method: req.method },
})
},
},
}
app.use(
errorHandler(errorConfig[process.env.NODE_ENV] || errorConfig.development)
)Integration with Router Middleware
Perfect integration with @verisure-italy/router-middleware:
import express from 'express'
import { createRouter, createRestResource } from '@verisure-italy/router-middleware'
import { errorHandler } from '@verisure-italy/error-handler-middleware'
import { User, userSchema } from '@verisure-italy/aaa-types'
const app = express()
// Router middleware
const userRoutes = createRestResource<User>({
dataModel: 'user',
basePath: '/users',
sharedOptions: {
secure: true,
acl: { allow: { roles: ['ROLE_AAA_ADMIN'] } },
},
methodOptions: {
create: {
validate: { body: userSchema.omit({ id: true }) },
},
},
})
app.use('/api', createRouter(userRoutes, { /* config */ }))
// Error handler (MUST be last)
app.use(errorHandler({
logErrors: true,
showDetails: process.env.NODE_ENV !== 'production',
}))Error handling flow:
Validation Error (from Zod in router-middleware)
- Middleware catches validation error
- Error handler formats with field-by-field details
- Returns 422 with detailed validation errors
Authentication Error (from authentication-middleware)
- Middleware catches auth error
- Error handler formats
- Returns 401 with clear auth message
Authorization Error (from ACL in router-middleware)
- Middleware catches forbidden error
- Error handler formats
- Returns 403 with permission message
Type Checking
The middleware provides TypeScript utilities to check error types:
import {
isDomainError,
isValidationError,
isAuthenticationError,
isAuthorizationError,
isNotFoundError,
isZodError,
} from '@verisure-italy/error-handler-middleware'
app.use(
errorHandler({
onError: (error) => {
if (isValidationError(error)) {
console.log('Validation failed:', error.errors)
} else if (isAuthenticationError(error)) {
console.log('Auth failed')
} else if (isZodError(error)) {
console.log('Zod validation issues:', error.issues)
}
},
})
)Best Practices
1. Always Place Last
The error handler MUST be the last middleware:
// ✅ Correct order
app.use(bodyParser.json())
app.use(authMiddleware)
app.use('/api', router)
app.use(errorHandler()) // Last!
// ❌ Wrong order
app.use(errorHandler()) // Too early!
app.use('/api', router)2. Different Configs for Environments
// ✅ Good
app.use(errorHandler({
showDetails: process.env.NODE_ENV !== 'production',
logErrors: process.env.NODE_ENV === 'development',
}))
// ❌ Bad - exposes sensitive info in production
app.use(errorHandler({
showDetails: true,
includeStackTrace: true,
}))3. Use Custom Error Handler for Monitoring
// ✅ Good - log to monitoring service
app.use(errorHandler({
onError: async (error, req) => {
if (process.env.NODE_ENV === 'production') {
await monitoring.captureException(error, {
tags: { path: req.path, method: req.method },
})
}
},
}))4. Graceful Degradation
// ✅ Good - fallback if error handler fails
app.use(errorHandler())
// Fallback handler
app.use((error, req, res, next) => {
res.status(500).json({ error: 'Something went wrong' })
})Examples
Complete Express App
import express from 'express'
import { createRouter, createRestResource } from '@verisure-italy/router-middleware'
import { dynamoAuthMiddleware } from '@verisure-italy/authentication-middleware'
import { errorHandler } from '@verisure-italy/error-handler-middleware'
import { User, userSchema } from '@verisure-italy/aaa-types'
const app = express()
// Body parser
app.use(express.json())
// Authentication
app.use(
dynamoAuthMiddleware({
dynamoConfig: { region: 'eu-west-1' },
tokenTableName: 'access_tokens',
userTableName: 'users',
})
)
// API routes
const userRoutes = createRestResource<User>({
dataModel: 'user',
basePath: '/users',
methods: ['create', 'read', 'update', 'list', 'delete'],
sharedOptions: {
secure: true,
acl: { allow: { roles: ['ROLE_AAA_ADMIN'] } },
},
methodOptions: {
create: {
validate: { body: userSchema.omit({ id: true }) },
},
update: {
validate: { body: userSchema.partial().omit({ id: true }) },
},
},
})
app.use(
'/api',
createRouter(userRoutes, {
dataModels: { user: { tableName: 'users', idField: 'id' } },
})
)
// 404 handler
app.use((req, res, next) => {
const error: any = new Error('Route not found')
error.statusCode = 404
error.code = 'NOT_FOUND'
next(error)
})
// Error handler (MUST be last!)
app.use(
errorHandler({
logErrors: true,
showDetails: process.env.NODE_ENV !== 'production',
onError: async (error, req) => {
// Log to monitoring service in production
if (process.env.NODE_ENV === 'production') {
await logger.error(error, {
path: req.path,
method: req.method,
})
}
},
})
)
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})Error Response Examples
Validation Error with Multiple Fields
{
"error": "Validation Error",
"message": "The request contains invalid data",
"statusCode": 422,
"code": "VALIDATION_ERROR",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users",
"details": [
{
"field": "username",
"message": "String must contain at least 1 character(s)",
"code": "too_small",
"value": ""
},
{
"field": "roles",
"message": "Array must contain at least 1 element(s)",
"code": "too_small",
"value": []
},
{
"field": "email",
"message": "Invalid email",
"code": "invalid_string",
"value": "not-an-email"
}
]
}Authentication Error
{
"error": "Authentication Error",
"message": "Token expired",
"statusCode": 401,
"code": "UNAUTHORIZED",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users"
}Authorization Error
{
"error": "Authorization Error",
"message": "You do not have permission to access this resource",
"statusCode": 403,
"code": "FORBIDDEN",
"timestamp": "2025-10-24T10:30:00.000Z",
"path": "/api/users/123"
}License
MIT
