Fundamentals
Express.js is a popular web application framework for Node.js that simplifies building web applications and APIs.
Middleware: Functions that access the request and response objects to modify, add data, or trigger other functions.
Router: A mini-app that only deals with routing. It can have its middleware and routing logic.
Handler: A function that handles a specific route or endpoint.
Error Middleware: Middleware functions that have an extra parameter for error handling.
graph
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
middleware1["๐ชMiddleware 1"]
middleware2["๐ชMiddleware 2"]
router["fa:fa-sitemap Router"]
routerMiddleware["๐ชRouter Middleware"]
handler["fa:fa-wrench Handler"]
errorMiddleware["๐จ Error Middleware"]
response["๐ต Response"]
end
request --> middleware1 --> middleware2 --> router
router --> routerMiddleware --> handler
handler --> errorMiddleware --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style middleware1 fill:#ccf,stroke:#f66,stroke-width:2px
style middleware2 fill:#ff9,stroke:#333,stroke-width:2px
style router fill:#ccf,stroke:#f66,stroke-width:2px
style routerMiddleware fill:#add8e6,stroke:#333,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
style errorMiddleware stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
classDef requestClass fill:#f9f,stroke:#333,stroke-width:2px;
classDef middlewareClass fill:#ccf,stroke:#f66,stroke-width:2px;
classDef routerClass fill:#ccf,stroke:#f66,stroke-width:2px;
classDef routerMiddlewareClass fill:#add8e6,stroke:#333,stroke-width:2px;
classDef handlerClass fill:#9cf,stroke:#333,stroke-width:2px;
classDef errorClass stroke:#333,stroke-width:2px;
classDef responseClass fill:#9cf,stroke:#333,stroke-width:2px;
Middleware
A request-response cycle in Express.js involves a series of middleware functions that execute sequentially. Each middleware can modify the request and response objects, end the request-response cycle, or call the next middleware in the stack.
graph
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
middleware1["๐ช Middleware 1"]
middleware2["๐ชMiddleware 2"]
endRequest["fa:fa-stop End Request"]
throwError["fa:fa-exclamation-triangle Error Middleware"]
end
request --> middleware1
middleware1 --> |chain| middleware2
middleware1 --> |end| endRequest
middleware1 --> |error| throwError
style request fill:#f9f,stroke:#333,stroke-width:2px
style middleware1 fill:#ccf,stroke:#f66,stroke-width:2px
style middleware2 fill:#ff9,stroke:#333,stroke-width:2px
style endRequest fill:#fcc,stroke:#333,stroke-width:2px
style throwError stroke:#333,stroke-width:2px
Middleware Functions: Execute sequentially, each modifying the request/response objects or ending the request-response cycle. Examples: Logging, authentication, parsing data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const express = require('express');
const app = express();
app.use((req, res, next) => { // ( ๐ด, ๐ต, ๐ช)
console.log('Middleware 1');
next();
});
app.use((req, res, next) => {
console.log('Middleware 2');
res.send('Hello, Middleware Flow!');
});
app.listen(3000);
|
Below is an example of how middleware functions involve in the request-response cycle.
graph TD
subgraph
cors1[cors ๐] --> passportInitialize["passport.initialize() (Passport) ๐"]
passportInitialize --> authLimiter["authLimiter (Rate Limiter) ๐ฆ"]
authLimiter --> routes["v1 Routes ๐ฃ๏ธ"]
routes --> docsRoute["Docs Route ๐"]
docsRoute --> notFoundHandler["404 Handler โ ๏ธ"]
notFoundHandler --> errorConverter["errorConverter โ๏ธ"]
errorConverter --> errorHandler["errorHandler ๐ซ"]
errorHandler --> response["Response โก๏ธ"]
style passportInitialize fill:#ccf,stroke:#f66,stroke-width:2px
style authLimiter fill:#ff9,stroke:#333,stroke-width:2px
style routes fill:#9cf,stroke:#333,stroke-width:2px
style docsRoute stroke:#333,stroke-width:2px
style notFoundHandler fill:#ccc,stroke:#333,stroke-width:2px
style errorConverter fill:#fcc,stroke:#333,stroke-width:2px
style errorHandler fill:#faa,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
end
subgraph Middleware["Middleware Flow"]
request["Request ๐"] --> morgan["Morgan (Logger) ๐"]
morgan --> helmet["Helmet (Security) ๐"]
helmet --> expressJson["express.json() ๐"]
expressJson --> expressUrlEncoded["express.urlencoded() ๐"]
expressUrlEncoded --> expressFileupload["express-fileupload ๐"]
expressFileupload --> xss["xss-clean ๐งน"]
xss --> mongoSanitize["express-mongo-sanitize ๐ก๏ธ"]
mongoSanitize --> compression["compression ๐๏ธ"]
compression --> cors["cors ๐"]
style request fill:#f9f,stroke:#333,stroke-width:2px
style morgan fill:#ccf,stroke:#f66,stroke-width:2px
style helmet fill:#ff9,stroke:#333,stroke-width:2px
style expressJson fill:#9cf,stroke:#333,stroke-width:2px
style expressUrlEncoded stroke:#333,stroke-width:2px
style expressFileupload fill:#ccc,stroke:#333,stroke-width:2px
style xss fill:#fcc,stroke:#333,stroke-width:2px
style mongoSanitize fill:#faa,stroke:#333,stroke-width:2px
style compression fill:#9cf,stroke:#333,stroke-width:2px
style cors fill:#afa,stroke:#333,stroke-width:2px
end
Routing
graph
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"] --> middleware1["๐ชMiddlewares"]
middleware1 --> router
subgraph router["fa:fa-sitemap Router"]
authRoute["/auth ๐"]
userRoute["/user ๐ง"]
clientRoute["/client ๐ข"]
remains["..."]
notFoundRoute["404"]
end
router --> errorMiddleware["๐จ Error Middleware"]
errorMiddleware --> response["๐ต Response"]
end
style request fill:#f9f,stroke:#333,stroke-width:2px
style middleware1 fill:#ccf,stroke:#f66,stroke-width:2px
style router fill:#add8e6,stroke:#333,stroke-width:2px
style authRoute fill:#9cf,stroke:#333,stroke-width:2px
style userRoute stroke:#333,stroke-width:2px
style clientRoute fill:#ccc,stroke:#333,stroke-width:2px
style notFoundRoute fill:#faa,stroke:#333,stroke-width:2px
style errorMiddleware stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
classDef requestClass fill:#f9f,stroke:#333,stroke-width:2px;
classDef middlewareClass fill:#ccf,stroke:#f66,stroke-width:2px;
classDef routerClass fill:#add8e6,stroke:#333,stroke-width:2px;
classDef routeHandlerClass fill:#9cf,stroke:#333,stroke-width:2px;
classDef errorClass stroke:#333,stroke-width:2px;
classDef responseClass fill:#9cf,stroke:#333,stroke-width:2px;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| const express = require('express');
const router = express.Router();
// Import Route Modules (Assume these contain route handlers)
const authRoute = require('./auth.route');
const userRoute = require('./user.route');
const clientRoute = require('./client.route');
// Mount Routes on the Router
router.use('/auth', authRoute); // Authentication routes (e.g., login, signup)
router.use('/user', userRoute); // User management routes
router.use('/client', clientRoute); // Client-related routes
// ... other routes (omitted for brevity)
// 404 Not Found Handler
router.use((req, res, next) => {
// ... (Logic for handling 404 errors)
});
module.exports = router;
// ... In your main app.js file:
const app = express();
// ... (Other middleware like body-parser, cors, etc.)
// Mount the Router
app.use('/api', router); // Prefix all routes with '/api'
// ... (Error handling middleware)
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
|
Pamaeters and Query Strings
- Route Parameters: Extracted from the URL path using a colon followed by the parameter name (e.g.,
/users/:id
). - Query Strings: Extracted from the URL query string using the
req.query
object (e.g., /users?name=John
).
1
2
3
4
5
6
7
8
9
10
11
| // Route Parameters
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.send(`User ID: ${id}`);
});
// Query Strings
app.get('/users', (req, res) => {
const { name } = req.query;
res.send(`User Name: ${name}`);
});
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
routeParams["/users/:id"]
queryParams["/users?name=John"]
routeHandler1["fa:fa-wrench Route Handler"]
routeHandler2["fa:fa-wrench Route Handler"]
end
request --> routeParams
routeParams --> routeHandler1
request --> queryParams
queryParams --> routeHandler2
style request fill:#f9f,stroke:#333,stroke-width:2px
style routeParams fill:#ccf,stroke:#f66,stroke-width:2px
style queryParams fill:#ff9,stroke:#333,stroke-width:2px
Priority of Routes
- Exact Match: Routes with exact matches take precedence over dynamic routes.
- Dynamic Routes: Routes with dynamic parameters (e.g.,
/users/:id
) are matched next. - Wildcard Routes: Routes with wildcards (e.g.,
/users/*
) are matched last.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| app.get('/users', (req, res) => {
res.send('All users');
});
app.get('/users/new', (req, res) => {
res.send('New user form');
});
app.get('/users/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
app.get('/users/*', (req, res) => {
res.send('Wildcard route');
});
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
usersRoute["/users"]
newUserRoute["/users/new"]
userIdRoute["/users/:id"]
wildcardRoute["/users/*"]
response["๐ต Response"]
end
request --> usersRoute
usersRoute --> response
request --> newUserRoute
newUserRoute --> response
request --> userIdRoute
userIdRoute --> response
request --> wildcardRoute
wildcardRoute --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style usersRoute fill:#ccf,stroke:#f66,stroke-width:2px
style newUserRoute fill:#ff9,stroke:#333,stroke-width:2px
style userIdRoute fill:#9cf,stroke:#333,stroke-width:2px
style wildcardRoute fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
Route Composition
- Route Composition: Use
express.Router()
to create modular, mountable route handlers. This allows you to define routes in separate files and mount them at any path.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// routes/auth.route.js
const express = require('express');
const router = express.Router();
router.post('/login', (req, res) => {
// Login logic
});
router.post('/signup', (req, res) => {
// Signup logic
});
module.exports = router;
// routes/user.route.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
// Get all users
});
router.get('/:id', (req, res) => {
// Get user by ID
});
module.exports = router;
// app.js
const express = require('express');
const app = express();
const authRoute = require('./routes/auth.route');
const userRoute = require('./routes/user.route');
app.use('/auth', authRoute);
app.use('/users', userRoute);
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
authRoute["/auth"]
userRoute["/users"]
authHandler["fa:fa-wrench Auth Handler"]
userHandler["fa:fa-wrench User Handler"]
end
request --> authRoute
authRoute --> authHandler
request --> userRoute
userRoute --> userHandler
style request fill:#f9f,stroke:#333,stroke-width:2px
style authRoute fill:#ccf,stroke:#f66,stroke-width:2px
style userRoute fill:#ff9,stroke:#333,stroke-width:2px
style authHandler fill:#9cf,stroke:#333,stroke-width:2px
style userHandler fill:#9cf,stroke:#333,stroke-width:2px
Views
graph
subgraph views["๐ผ๏ธ Views"]
engine["Engine"] --> template["๐ Template"]
template --> data["Data"]
data --> rendered["๐๏ธ Rendered HTML"]
end
- Templates: Use template engines like Pug, EJS, or Handlebars to create dynamic HTML.
1
2
3
4
5
6
7
8
9
| const express = require('express');
const app = express();
app.set('view engine', 'pug');
app.get('/', (req, res) => {
res.render('index', { title: 'Express', message: 'Hello there!' });
});
app.listen(3000);
|
- Rendering: Generates and returns HTML based on the templates and data provided.
1
2
3
4
5
6
| // views/index.pug
html
head
title= title
body
h1= message
|
Static Files
Static files are assets that donโt change dynamically, such as images, CSS stylesheets, and client-side JavaScript files. express.static()
is a middleware function, meaning it intercepts requests before they reach your route handlers.
1
2
3
4
5
6
| const express = require('express');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.listen(3000);
|
Directory: Specify the directory from which to serve static assets.
graph LR
root["my-app/ ๐"] --> public["public/ ๐"];
public --> images["images/ ๐ผ๏ธ"];
public --> css["css/ ๐จ"];
public --> js["js/ โ๏ธ"];
public --> csv["data.csv ๐"];
classDef folder fill:#f9f,stroke:#333,stroke-width:2px;
classDef file fill:#ccf,stroke:#f66,stroke-width:2px;
style root fill:#F0E68C
style public fill:#90EE90
style images fill:#ADD8E6
style css fill:#F08080
style js fill:#98FB98
style csv fill:#F0F8FF
Use Stream
to download files in public directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const fs = require('fs');
app.get('/download-csv', (req, res) => {
const filePath = path.join(__dirname, 'public', 'path/to/your/file.csv');
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
// Set headers for download
res.setHeader('Content-Disposition', 'attachment; filename=file.csv');
res.setHeader('Content-Type', 'text/csv');
// Pipe the file to the response
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
});
|
Logging
Log errors to the console or a file for debugging and monitoring. Datadog, Sentry, or other services can be used for more advanced error logging.
1
2
3
4
5
| function logErrors(err, req, res, next) {
console.error(err.stack); // Log to console in development
// You can replace this with logging to a file or external service
next(err);
}
|
Modern applications need standardized logging to monitor and debug issues effectively. Winston and Morgan are popular logging libraries for Node.js applications. Key requirements for logging include:
- Structured Format: Log messages should be in a structured format (e.g., JSON) for easy parsing and analysis.
- Log Levels: Different log levels (e.g., info, warn, error) help categorize log messages based on severity.โ
- Timestamps: Include timestamps in log messages to track when events occurred.
- Context: Additional metadata.
- Monitoring: Centralized logging solutions (e.g., Datadog, Sentry) for monitoring and alerting.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // src/config/logger.js
const winston = require('winston');
const path = require('path');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
// Console transport
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// Error file transport
new winston.transports.File({
filename: path.join(__dirname, '../../logs/error.log'),
level: 'error'
}),
// Combined file transport
new winston.transports.File({
filename: path.join(__dirname, '../../logs/combined.log')
})
]
});
module.exports = logger;
|
Log Outputs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
// Example usage in your application:
try {
// Some operation that might fail
throw new Error('Database connection failed');
} catch (error) {
// Error level (most serious issues)
logger.error('Database connection error', {
error: error.message,
stack: error.stack,
database: 'postgres'
});
/* Output:
{
"level": "error",
"message": "Database connection error",
"timestamp": "2024-11-08T10:30:45.123Z",
"error": "Database connection failed",
"stack": "Error: Database connection failed\n at ...",
"database": "postgres"
}
*/
// Warning level (potential issues)
logger.warn('High API latency detected', {
latency: 1500,
endpoint: '/api/users'
});
/* Output:
{
"level": "warn",
"message": "High API latency detected",
"timestamp": "2024-11-08T10:30:45.123Z",
"latency": 1500,
"endpoint": "/api/users"
}
*/
// Info level (normal operations)
logger.info('API request received', {
method: 'GET',
path: '/api/users'
});
/* Output:
{
"level": "info",
"message": "API request received",
"timestamp": "2024-11-08T10:30:45.123Z",
"method": "GET",
"path": "/api/users"
}
*/
}
|
Error Handling
Logging Errors
graph TD
routeHandler --error--> nextError["fa:fa-exclamation next(err)"]
request["๐ด"] --> middleware1["๐ชMiddleware 1"] --error--> nextError["fa:fa-door-open next"]
middleware2 --error--> nextError
middleware1 --> middleware2["๐ชMiddleware 2"]
middleware2 --> routeHandler["fa:fa-wrench Route Handler"]
routeHandler --> response["๐ต"]
nextError --> logErrors["logErrors ๐"]
logErrors --> clientErrorHandler["clientErrorHandler ๐ฅ"]
clientErrorHandler --> errorHandler["errorHandler ๐ซ"]
errorHandler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style middleware1 fill:#ccf,stroke:#f66,stroke-width:2px
style middleware2 fill:#ff9,stroke:#333,stroke-width:2px
style routeHandler fill:#ccf,stroke:#f66,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
style nextError stroke:#333,stroke-width:2px
style logErrors fill:#a0d2eb,stroke:#333,stroke-width:2px
style clientErrorHandler fill:#f0e68c,stroke:#333,stroke-width:2px
style errorHandler stroke:#333,stroke-width:2px
For small to medium applications, itโs better to centralize logging in middleware for consistency and easier maintenance. Hereโs why:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
| // โ First Version - Don't mix logging and throwing
const logger = require('../config/logger');
class UserService {
async getUser(id) {
const user = await User.findById(id);
if (!user.isActive) {
// Bad practice: Logging before throwing
logger.error('Inactive user attempted access', {
userId: user.id,
status: user.status
});
throw new Error('Inactive user access denied');
}
return user;
}
}
// โ
Second Version - Better approach with centralized logging
// errors/AppError.js
class AppError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}
class InactiveUserError extends AppError {
constructor(userId, status) {
super('Inactive user access denied', 'INACTIVE_USER');
this.userId = userId;
this.status = status;
}
}
// services/userService.js
class UserService {
async getUser(id) {
const user = await User.findById(id);
if (!user.isActive) {
throw new InactiveUserError(user.id, user.status);
}
return user;
}
}
// middleware/errorMiddleware.js
const logger = require('../config/logger');
function logErrors(err, req, res, next) {
logger.error('Error occurred', {
// Error details
error: {
message: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
code: err.code || 'UNKNOWN_ERROR',
// Include any custom properties from custom error classes
...(err instanceof InactiveUserError ? {
userId: err.userId,
status: err.status
} : {})
},
// Request context
request: {
path: req.path,
method: req.method,
params: req.params,
query: req.query,
ip: req.ip
},
// User context (if available)
user: req.user ? {
id: req.user.id,
email: req.user.email
} : 'anonymous',
// Timestamp and environment
timestamp: new Date().toISOString(),
env: process.env.NODE_ENV
});
next(err);
}
// Example of how to use in Express app:
app.use(logErrors);
|
Benefits of centralized logging:
- Single place to modify logging logic
- Consistent log format
- Easier to maintain
- Cleaner service code
- Separation of concerns
The only exception might be audit logging or business-specific logging that needs to be in the service layer.
Handling different types of errors
To handle different types of errors, you can create custom error classes that extend the built-in Error class. This approach allows you to categorize errors based on their type and provide a consistent error structure.
graph TD
subgraph errorClasses["๐จ Error Classes"]
appError["AppError"]
databaseError["DatabaseError"]
validationError["ValidationError"]
notFoundError["NotFoundError"]
authError["AuthError"]
end
subgraph errorMiddleware["๐จ Error Middleware"]
logErrors["logErrors ๐"]
errorHandler["errorHandler ๐ซ"]
end
appError --> databaseError
appError --> validationError
appError --> notFoundError
appError --> authError
logErrors --> errorHandler
style appError fill:#f9f,stroke:#333,stroke-width:2px
style databaseError fill:#ccf,stroke:#f66,stroke-width:2px
style validationError fill:#ff9,stroke:#333,stroke-width:2px
style notFoundError fill:#9cf,stroke:#333,stroke-width:2px
style authError fill:#9cf,stroke:#333,stroke-width:2px
style logErrors fill:#ccf,stroke:#f66,stroke-width:2px
style errorHandler fill:#ff9,stroke:#333,stroke-width:2px
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
| // src/utils/AppError.js
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code;
}
}
class DatabaseError extends AppError {
constructor(message = 'Database error occurred') {
super(message, 500, 'DATABASE_ERROR');
}
}
class ValidationError extends AppError {
constructor(message = 'Validation failed') {
super(message, 400, 'VALIDATION_ERROR');
}
}
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404, 'NOT_FOUND');
}
}
class AuthError extends AppError {
constructor(message = 'Authentication failed') {
super(message, 401, 'AUTH_ERROR');
}
}
module.exports = {
AppError,
DatabaseError,
ValidationError,
NotFoundError,
AuthError
};
// Usage in your service
// userService.js
const { DatabaseError, NotFoundError } = require('../utils/AppError');
class UserService {
async getUser(id) {
try {
const user = await User.findById(id);
if (!user) {
throw new NotFoundError('User not found');
}
return user;
} catch (error) {
if (error.name === 'MongoError') {
throw new DatabaseError('Database query failed');
}
throw error;
}
}
}
// Then in your error middleware
// middleware/errorMiddleware.js
const logger = require('../config/logger');
function logErrors(err, req, res, next) {
logger.error('Error occurred', {
error: {
message: err.message,
code: err.code,
statusCode: err.statusCode,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
},
request: {
path: req.path,
method: req.method
},
user: req.user?.id || 'anonymous'
});
next(err);
}
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
status: 'error',
code: err.code || 'UNKNOWN_ERROR',
message: err.message
});
}
|
This approach:
- Keeps error types organized
- Provides consistent error structure
- Makes error handling predictable
- Easy to add new error types
- Simple to use in services
Q&A
How does Express.js determine whether to call the next middleware or an error-handling middleware?
It depends on how the next
function is called:
next()
: Without arguments, it proceeds to the next regular middleware.next(err)
: With an error argument, it skips to the error-handling middleware.
1
2
3
4
5
6
7
8
9
10
11
| app.use((req, res, next) => {
next(); // Calls the next regular middleware
});
app.use((req, res, next) => {
next(new Error('Error occurred')); // Calls the error-handling middleware
});
app.use((err, req, res, next) => {
res.status(500).send('Something broke!');
});
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
middleware1["๐ตMiddleware 1"]
middleware2["๐ชMiddleware 2"]
errorHandler["โ ๏ธ Error Handler"]
finalHandler["โ
Final Handler"]
end
request --> |"next()"| middleware1
middleware1 --> |"next()"| middleware2
middleware1 --> |"next(err)"| errorHandler
middleware2 --> |"next()"| finalHandler
middleware2 --> |"next(err)"| errorHandler
style request fill:#f9f,stroke:#333,stroke-width:2px
style middleware1 fill:#ccf,stroke:#f66,stroke-width:2px
style middleware2 fill:#ff9,stroke:#333,stroke-width:2px
style errorHandler fill:#f66,stroke:#333,stroke-width:2px
style finalHandler fill:#cfc,stroke:#333,stroke-width:2px
What are the differences between req.query and req.params ?
req.query
: Contains the query parameters in the URL (e.g., /users?name=John&age=30
).req.params
: Contains route parameters defined in the route path (e.g., /users/:id
).
1
2
3
4
5
6
7
8
9
| app.get('/users', (req, res) => {
const { name, age } = req.query;
res.send(`Name: ${name}, Age: ${age}`);
});
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.send(`User ID: ${id}`);
});
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
queryRoute["/users?name=John&age=30"]
paramsRoute["/users/:id"]
queryHandler["fa:fa-wrench Query Handler"]
paramsHandler["fa:fa-wrench Params Handler"]
response["๐ต Response"]
end
request --> queryRoute
queryRoute --> queryHandler
queryHandler --> response
request --> paramsRoute
paramsRoute --> paramsHandler
paramsHandler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style queryRoute fill:#ccf,stroke:#f66,stroke-width:2px
style paramsRoute fill:#ff9,stroke:#333,stroke-width:2px
style queryHandler fill:#9cf,stroke:#333,stroke-width:2px
style paramsHandler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to parse the request body?
express.json()
: Middleware to parse JSON bodies.express.urlencoded()
: Middleware to parse URL-encoded bodies.express.text()
: Middleware to parse text bodies.express.raw()
: Middleware to parse raw bodies.
1
2
3
4
5
6
7
8
9
10
11
12
| const express = require('express');
const app = express();
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
app.post('/users', (req, res) => {
const { name, age } = req.body;
res.send(`Name: ${name}, Age: ${age}`);
});
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
jsonBody["{ name: 'John', age: 30 }"]
urlEncodedBody["name=John&age=30"]
jsonMiddleware["{ } express.json()"]
urlEncodedMiddleware["& express.urlencoded() "]
response["๐ต Response"]
end
request --> jsonBody
jsonBody --> jsonMiddleware
jsonMiddleware --> response
request --> urlEncodedBody
urlEncodedBody --> urlEncodedMiddleware
urlEncodedMiddleware --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style jsonBody fill:#ccf,stroke:#f66,stroke-width:2px
style urlEncodedBody fill:#ff9,stroke:#333,stroke-width:2px
style jsonMiddleware fill:#9cf,stroke:#333,stroke-width:2px
style urlEncodedMiddleware fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
Explain the order of router precedence ?
- Exact Match: Routes with exact matches take precedence over dynamic routes.
- Dynamic Routes: Routes with dynamic parameters (e.g.,
/users/:id
) are matched next. - Wildcard Routes: Routes with wildcards (e.g.,
/users/*
) are matched last.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| app.get('/users', (req, res) => {
res.send('All users');
});
app.get('/users/new', (req, res) => {
res.send('New user form');
});
app.get('/users/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
app.get('/users/*', (req, res) => {
res.send('Wildcard route');
});
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
usersRoute["/users"]
newUserRoute["/users/new"]
userIdRoute["/users/:id"]
wildcardRoute["/users/*"]
response["๐ต Response"]
end
request --> usersRoute
usersRoute --> response
request --> newUserRoute
newUserRoute --> response
request --> userIdRoute
userIdRoute --> response
request --> wildcardRoute
wildcardRoute --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style usersRoute fill:#ccf,stroke:#f66,stroke-width:2px
style newUserRoute fill:#ff9,stroke:#333,stroke-width:2px
style userIdRoute fill:#9cf,stroke:#333,stroke-width:2px
style wildcardRoute fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to handle file uploads ?
express-fileupload
: Middleware to handle file uploads.req.files
: Object containing uploaded files.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
app.use(fileUpload());
app.post('/upload', (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('No files were uploaded.');
}
let uploadedFile = req.files.file; // assuming the form field name is 'file'
// Read the content of the file
const fileContent = uploadedFile.data.toString();
// Process the file content
const updatedContent = processFile(fileContent);
// ...
res.send(updatedContent);
});
|
How to protect SQL Injection?
1
2
3
4
5
6
7
8
9
10
| const express = require('express');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const app = express();
app.use(mongoSanitize());
app.use(xss());
///
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
mongoSanitize["express-mongo-sanitize ๐ก๏ธ"]
xssClean["xss-clean ๐งน"]
handler["fa:fa-wrench Handler"]
response["๐ต Response"]
end
request --> mongoSanitize
mongoSanitize --> xssClean
xssClean --> handler
handler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style mongoSanitize fill:#ccf,stroke:#f66,stroke-width:2px
style xssClean fill:#ff9,stroke:#333,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to implement rate limiting?
express-rate-limit
: Middleware to limit the number of requests from an IP address.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes'
});
app.use('/auth', authLimiter);
app.post('/auth/login', (req, res) => {
// Handle login logic
});
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
authRoute["/auth/login"]
authLimiter["authLimiter (Rate Limiter) ๐ฆ"]
handler["fa:fa-wrench Handler"]
response["๐ต Response"]
end
request --> authRoute
authRoute --> authLimiter
authLimiter --> handler
handler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style authRoute fill:#ccf,stroke:#f66,stroke-width:2px
style authLimiter fill:#ff9,stroke:#333,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to handle versioning in APIs?
- Route Prefixing: Use a common prefix for all routes of a specific version.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const express = require('express');
const app = express();
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users', (req, res) => {
res.send('Users v1');
});
v2Router.get('/users', (req, res) => {
res.send('Users v2');
});
app.use('/v1', v1Router);
app.use('/v2', v2Router);
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
v1Route["/v1/users"]
v2Route["/v2/users"]
v1Handler["fa:fa-wrench Users v1 Handler"]
v2Handler["fa:fa-wrench Users v2 Handler"]
response["๐ต Response"]
end
request --> v1Route
v1Route --> v1Handler
v1Handler --> response
request --> v2Route
v2Route --> v2Handler
v2Handler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style v1Route fill:#ccf,stroke:#f66,stroke-width:2px
style v2Route fill:#ff9,stroke:#333,stroke-width:2px
style v1Handler fill:#9cf,stroke:#333,stroke-width:2px
style v2Handler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to handle CORS ?
cors
: Middleware to enable Cross-Origin Resource Sharing (CORS) in Express.
By default, browsers restrict cross-origin HTTP requests for security reasons, meaning a request from a different origin (like a frontend running on http://localhost:3000 trying to access an API on http://localhost:5000) would be blocked.
With app.use(cors()), youโre allowing the server to accept requests from different origins, making it suitable for a frontend application hosted on a different domain than your API.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| const express = require('express');
const cors = require('cors');
const app = express();
// Configuration object based on environment
const corsOptions = {
origin: function (origin, callback) {
// List of allowed origins based on environment
const allowedOrigins = {
production: ['https://mywebside.com'],
development: ['https://dev.myside.com'],
local: ['http://localhost:3000']
};
// Get current environment from NODE_ENV (defaults to 'development')
const environment = process.env.NODE_ENV || 'development';
const whitelist = allowedOrigins[environment];
// Allow requests with no origin (like mobile apps, curl, postman)
if (!origin) {
return callback(null, true);
}
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // Allow credentials (cookies, authorization headers, etc)
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};
// Apply CORS middleware
app.use(cors(corsOptions));
// Example route
app.get('/', (req, res) => {
res.json({ message: 'Hello World!' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`);
});
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
corsRoute["/users"]
cors["cors ๐"]
handler["fa:fa-wrench Users Handler"]
response["๐ต Response"]
end
request --> corsRoute
corsRoute --> cors
cors --> handler
handler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style corsRoute fill:#ccf,stroke:#f66,stroke-width:2px
style cors fill:#ff9,stroke:#333,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to document APIs?
- Swagger/OpenAPI: Use tools like Swagger UI or OpenAPI to document your APIs.
1
2
3
4
5
6
7
8
| const express = require('express');
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');
const app = express();
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
swaggerRoute["/api-docs"]
swaggerUi["swagger-ui-express ๐"]
swaggerDocument["swagger.json ๐"]
response["๐ต Response"]
end
request --> swaggerRoute
swaggerRoute --> swaggerUi
swaggerUi --> swaggerDocument
swaggerDocument --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style swaggerRoute fill:#ccf,stroke:#f66,stroke-width:2px
style swaggerUi fill:#ff9,stroke:#333,stroke-width:2px
style swaggerDocument fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to manage environment variables ?
dotenv
: Package to load environment variables from a .env
file.
1
2
3
4
5
6
7
8
9
10
| require('dotenv').config();
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
dotenv["dotenv ๐"]
handler["fa:fa-wrench Handler \n process.env"]
end
style dotenv fill:#ccf,stroke:#f66,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
How to nest routers?
- Router Nesting: Mount routers within other routers to create a nested routing structure.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const express = require('express');
const app = express();
const userRouter = express.Router();
const profileRouter = express.Router();
userRouter.use('/profile', profileRouter);
profileRouter.get('/', (req, res) => {
res.send('Profile');
});
app.use('/users', userRouter);
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
userRoute["/users"]
profileRoute["/profile"]
profileHandler["fa:fa-wrench Profile Handler"]
response["๐ต Response"]
end
request --> userRoute
userRoute --> profileRoute
profileRoute --> profileHandler
profileHandler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style userRoute fill:#ccf,stroke:#f66,stroke-width:2px
style profileRoute fill:#ff9,stroke:#333,stroke-width:2px
style profileHandler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width
How to compress responses in Express.js?
compression
: Middleware to compress responses using gzip or deflate.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const express = require('express');
const compression = require('compression');
const app = express();
app.use(compression());
app.get('/users', (req, res) => {
res.send('Users');
});
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
compression["compression ๐๏ธ"]
handler["fa:fa-wrench Handler"]
response["๐ต Response"]
end
request --> compression
compression --> handler
handler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style compression fill:#ccf,stroke:#f66,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How to validate request data ?
express-validator
: Middleware to validate and sanitize request data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();
app.post('/users',
body('email').isEmail(),
body('password').isLength({ min: 6 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.send('User created');
});
app.listen(3000);
|
graph TD
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด Request"]
validationRoute["/users"]
validationMiddleware["express-validator ๐ก๏ธ"]
handler["fa:fa-wrench Handler"]
response["๐ต Response"]
end
request --> validationRoute
validationRoute --> validationMiddleware
validationMiddleware --> handler
handler --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style validationRoute fill:#ccf,stroke:#f66,stroke-width:2px
style validationMiddleware fill:#ff9,stroke:#333,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
How do you implement rate limiting in Express.js?
express-rate-limit
: Middleware to limit the number of requests from an IP address.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes'
});
app.use('/auth', authLimiter);
app.post('/auth/login', (req, res) => {
// Handle login logic
});
app.listen(3000);
|
Whatโs the recommended way to handle async errors in Express route handlers?
Using async/await with express-async-handler is a common pattern to handle async errors in Express route handlers. This library wraps route handlers with a try-catch block and forwards errors to the error-handling middleware.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
const express = require('express');
const asyncHandler = require('express-async-handler');
const app = express();
app.get('/route', asyncHandler(async (req, res) => {
// Simulate an async operation
const result = await someAsyncFunction();
res.send(result);
}));
// Error-handling middleware
app.use((err, req, res, next) => {
res.status(500).json({ message: err.message });
});
function someAsyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Something went wrong!")), 1000);
});
}
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
|
How to implement session management in Express.js?
express-session
: Middleware to manage sessions in Express.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| const express = require('express');
const session = require('express-session');
const app = express();
app.use(session ({
secret
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}));
app.get('/', (req, res) => {
if (req.session.views) {
req.session.views++;
} else {
req.session.views = 1;
}
res.send(`Views: ${req.session.views}`);
});
app.listen(3000);
|
How to Redirect in Express.js?
res.redirect()
: Method to redirect to a different URL.
1
2
3
4
5
6
7
8
9
10
11
12
| const express = require('express');
const app = express();
app.get('/old', (req, res) => {
res.redirect('/new');
});
app.get('/new', (req, res) => {
res.send('New route');
});
app.listen(3000);
|
Keywords To Remember
graph
subgraph expressApp["fa:fa-server Express.js Application"]
request["๐ด "]
middleware1["๐ช"]
middleware2["๐ช"]
router["fa:fa-sitemap"]
routerMiddleware["๐ช"]
handler["fa:fa-wrench "]
errorMiddleware["๐จ"]
response["๐ต "]
end
request --> middleware1 --> middleware2 --> router
router --> routerMiddleware --> handler
handler --> errorMiddleware --> response
style request fill:#f9f,stroke:#333,stroke-width:2px
style middleware1 fill:#ccf,stroke:#f66,stroke-width:2px
style middleware2 fill:#ff9,stroke:#333,stroke-width:2px
style router fill:#ccf,stroke:#f66,stroke-width:2px
style routerMiddleware fill:#add8e6,stroke:#333,stroke-width:2px
style handler fill:#9cf,stroke:#333,stroke-width:2px
style errorMiddleware stroke:#333,stroke-width:2px
style response fill:#9cf,stroke:#333,stroke-width:2px
classDef requestClass fill:#f9f,stroke:#333,stroke-width:2px;
classDef middlewareClass fill:#ccf,stroke:#f66,stroke-width:2px;
classDef routerClass fill:#ccf,stroke:#f66,stroke-width:2px;
classDef routerMiddlewareClass fill:#add8e6,stroke:#333,stroke-width:2px;
classDef handlerClass fill:#9cf,stroke:#333,stroke-width:2px;
classDef errorClass fill:#f61,stroke:#333,stroke-width:2px;
classDef responseClass fill:#9cf,stroke:#333,stroke-width:2px;