MERN - Express.js Fundamentals
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');
});
- Modular Routes: Each route module (authRoute, userRoute, clientRoute) is responsible for a specific set of endpoints, promoting organization and maintainability.
Router Middleware: You can add middleware functions directly to the router using router.use(). These middleware will only apply to the routes defined within this router.
- 404 Handler: The router.use() at the end acts as a catch-all route to handle requests that donβt match any defined routes. It would typically send a βNot Foundβ (404) response.
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
// β Don't scatter logs in services
// userService.js
const logger = require('../config/logger');
class UserService {
async getUser(id) {
const user = await User.findById(id);
// Logging scattered throughout services
if (!user.isActive) {
logger.error('Inactive user attempted access', { // Don't do this
userId: user.id,
status: user.status
});
}
return user;
}
}
// β
Better: Throw errors and let middleware handle logging
// userService.js
class UserService {
async getUser(id) {
const user = await User.findById(id);
if (!user.isActive) {
throw new Error('Inactive user access denied'); // Just throw error
}
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'
},
// 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);
}
module.exports = 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?
express-mongo-sanitize
: Middleware to sanitize user input and prevent NoSQL injection.xss-clean
: Middleware to sanitize user input and prevent XSS attacks.
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
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;