Express.js Fundamentals
1. What is Express.js and its core principles?
Express.js is a minimalist, flexible, and fast Node.js web application framework that provides a robust set of features for web and mobile applications. Its core principles include being unopinionated, giving developers freedom in structuring their applications, middleware-centric for handling requests, and routing-focused for defining application endpoints. It simplifies the process of building robust APIs and web servers by abstracting much of Node.js's low-level HTTP module complexity, making development faster and more efficient.
2. How do you set up a basic Express server?
Setting up a basic Express server involves installing Express, requiring it in your main file, creating an application instance, defining a route, and then starting the server to listen on a specific port. This minimal setup creates a functional HTTP server capable of responding to requests on the defined endpoint.
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello from Express!');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
3. Explain routing in Express.js.
Routing in Express.js defines how an application responds to client requests to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, etc.). Each route can have one or more handler functions that are executed when the route is matched. It allows mapping different URLs to specific functions that process the request and send a response. This structure helps organize the application's logic based on requested resources.
app.get('/users/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
app.post('/products', (req, res) => {
res.status(201).send('Product created');
});
4. What is the purpose of req and res objects?
The req (request) and res (response) objects are fundamental to Express.js handlers. The req object represents the incoming HTTP request from the client and contains properties like request parameters (req.params), query strings (req.query), body data (req.body), headers (req.headers), and user information. The res object represents the HTTP response that the Express app sends back to the client. It provides methods to send data (res.send(), res.json()), set status codes (res.status()), redirect (res.redirect()), and set headers (res.set()).
5. How do you handle different HTTP methods?
Express.js provides dedicated methods for handling various HTTP verbs: app.get(), app.post(), app.put(), app.delete(), app.patch(), and app.all(). Each method corresponds to an HTTP request method and takes a path and one or more handler functions. app.all() is a special routing method that matches all HTTP verbs for a given path, useful for applying middleware or logging before specific method handlers.
6. What is app.listen() for?
app.listen() is used to bind the application to a specific network port and start listening for incoming HTTP connections. It essentially launches the Express server. This method takes the port number as the first argument and an optional callback function that executes once the server starts successfully. It's the final step to make your Express application accessible over the network.
const app = require('express')();
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Express server running on http://localhost:${port}`);
});
7. Differentiate between app.use() and app.METHOD() (e.g., app.get()).
app.use() is primarily for middleware and applies to all HTTP methods or paths that match. It executes for any request that passes through that path, regardless of the HTTP verb. app.METHOD() (like app.get(), app.post(), app.put()) is used for specific route handling and only executes for requests that match the specified HTTP method and path.
| Feature | app.use() | app.METHOD() (e.g., app.get()) |
|---|---|---|
| Purpose | Attaching middleware, serving static files | Defining specific route handlers |
| HTTP Methods | All methods by default (or specific via path prefix) | Specific HTTP method (GET, POST, PUT, DELETE, etc.) |
| Path Matching | Prefix matching (e.g., /api matches /api/users) | Exact path matching (e.g., /users matches only /users) |
| Execution Order | Executes for all requests matching prefix, before method-specific handlers | Executes only if method and path match exactly |
Middleware and Routing
8. Define middleware in Express.js. How does it work?
Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle. They can execute code, make changes to the request and response objects, end the request-response cycle, or call the next middleware in the stack. Middleware forms a chain, processing requests sequentially. They are fundamental for tasks like logging, authentication, data parsing, and error handling.
9. Give examples of built-in Express middleware.
Express itself doesn't come with many built-in middleware functions beyond express.static for serving static files, express.json for parsing JSON request bodies, and express.urlencoded for parsing URL-encoded request bodies. These are commonly used to add essential functionalities to an Express application.
const express = require('express');
const app = express();
app.use(express.json()); // Parses JSON bodies
app.use(express.urlencoded({ extended: true })); // Parses URL-encoded bodies
app.use(express.static('public')); // Serves static files from 'public' directory
// Other routes and middleware go here
10. How do you create a custom middleware?
A custom middleware function is essentially a function that takes (req, res, next) as arguments. Inside the function, you perform your logic (e.g., logging, validation, authentication). If the middleware needs to pass control to the next middleware in the stack, it must call next(). If it handles the request completely (e.g., sends a response), it should not call next().
const loggerMiddleware = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
next(); // Pass control to the next middleware/route handler
};
app.use(loggerMiddleware); // Apply globally
11. What is a router in Express.js, and why use it?
An Express Router is a mini-application that acts as a self-contained set of routes and middleware. It allows you to modularize your route definitions, separating concerns and making your application more organized and maintainable, especially in larger projects. You can define routes on a router instance and then mount that router at a specific path using app.use(). This prevents cluttering the main app object with too many route definitions.
const express = require('express');
const router = express.Router();
// Define routes on the router
router.get('/', (req, res) => res.send('Users homepage'));
router.get('/:id', (req, res) => res.send(`User ID: ${req.params.id}`));
module.exports = router; // Export the router
// In main app.js:
// const userRouter = require('./routes/users');
// app.use('/users', userRouter); // Mount the router at /users path
12. Explain route parameters and query strings.
Route parameters are named URL segments used to capture values specified at their position in the URL. They are typically denoted by a colon (:) followed by the parameter name in the route path (e.g., /users/:id). Their values are accessible via req.params. Query strings are key-value pairs appended to the URL after a question mark (?) and are used for optional parameters, filtering, or pagination (e.g., /products?category=electronics&limit=10). Their values are accessible via req.query.
13. How do you serve static files in Express?
Express.js provides the express.static middleware to serve static assets like HTML files, CSS stylesheets, JavaScript files, and images. You specify the directory from which to serve these files. When a request comes in, Express looks for a matching file in the specified static directory and, if found, serves it.
const express = require('express');
const app = express();
// Serve files from the 'public' directory
// e.g., if 'public/index.html' exists, it will be served at '/'
app.use(express.static('public'));
// You can also provide a virtual path prefix:
// app.use('/static', express.static('public'));
// Now, files in 'public' are accessed via '/static/...'
app.listen(3000);
14. What is next() and its importance in middleware?
next() is a function passed as the third argument to middleware functions. Its primary importance is to pass control to the next middleware function in the stack. If a middleware function does not send a response or call next(), the request will be left hanging, and subsequent middleware or route handlers for that request will never be executed. next() is crucial for enabling the chain of command in the Express middleware pipeline.
15. How can you apply middleware conditionally?
You can apply middleware conditionally by checking conditions within the middleware itself and calling next() or by using conditional logic before applying the middleware. Common approaches include:
- Inside the middleware: If a condition isn't met, call
next()to skip logic orres.status().send()to terminate. - Route-specific middleware: Apply middleware only to specific routes or groups of routes.
- Custom logic with
app.use(): Pass an array of middleware, or use a function that returns a middleware.
const authMiddleware = (req, res, next) => {
if (req.headers.authorization === 'Bearer TOKEN') {
next();
} else {
res.status(401).send('Unauthorized');
}
};
app.get('/admin', authMiddleware, (req, res) => {
res.send('Admin content');
});
Error Handling and Security
16. How do you handle errors in Express.js?
Express.js handles errors using error-handling middleware functions that accept four arguments: (err, req, res, next). When an error occurs synchronously in a route handler or middleware, or is passed to next(err), Express will skip all subsequent middleware and routing functions and pass the control to the first error-handling middleware. This centralized approach allows for consistent error responses, logging, and status code setting.
app.use((err, req, res, next) => {
console.error(err.stack); // Log the error stack
res.status(err.statusCode || 500).json({
message: err.message || 'Something broke!',
error: process.env.NODE_ENV === 'production' ? {} : err
});
});
17. Differentiate between synchronous and asynchronous error handling.
Synchronous errors (e.g., throw new Error('sync error')) inside Express route handlers or middleware are automatically caught by Express's default error handler or custom error-handling middleware.
Asynchronous errors (e.g., errors in Promises or callbacks) are not automatically caught by Express. For asynchronous errors, you must explicitly pass them to the next() function (next(err)) to forward them to the error-handling middleware, or wrap your async functions in a try...catch block and call next(err) within the catch. Modern Express apps often use a utility like express-async-handler or simple try/catch with async/await.
18. What is the "catch-all" error handler?
The "catch-all" error handler is the final error-handling middleware defined in your Express application, typically at the very end of your middleware stack, after all other app.use() and route definitions. Its purpose is to catch any errors that might have occurred in previous middleware or route handlers and were passed to next(err). It acts as a fallback, ensuring that no unhandled errors crash the server or result in a generic, unhelpful client response.
19. Name common security vulnerabilities in Express apps and how to mitigate them.
Common vulnerabilities include:
- XSS (Cross-Site Scripting): Sanitize user input (e.g., using
dompurifyfor HTML) and escape output before rendering. - CSRF (Cross-Site Request Forgery): Implement CSRF tokens (e.g.,
csurfmiddleware). - SQL Injection/NoSQL Injection: Use parameterized queries or ORMs/ODMs that handle escaping (e.g., Prisma, Mongoose).
- Brute Force Attacks: Implement rate limiting (
express-rate-limit). - Missing Security Headers: Use
helmetmiddleware to set various HTTP headers for protection (X-XSS-Protection, Strict-Transport-Security, etc.). - Insecure Dependencies: Regularly audit dependencies using tools like
npm audit. - Information Disclosure: Avoid sending stack traces or sensitive error details in production.
20. How would you secure API endpoints (authentication/authorization)?
Securing API endpoints involves authentication (verifying user identity) and authorization (determining if an authenticated user has permission). Authentication:
- Token-based (JWT): Users authenticate once, receive a JSON Web Token, and send it with subsequent requests in the
Authorizationheader. This is stateless and scalable. - OAuth 2.0/OpenID Connect: For third-party authentication. Authorization:
- Role-Based Access Control (RBAC): Assign roles (e.g., 'admin', 'user') to users and define permissions based on roles.
- Attribute-Based Access Control (ABAC): More granular, defines permissions based on attributes of the user, resource, and environment. Middleware can be used to check tokens and roles before allowing access to routes.
Performance and Scalability
21. How can you improve the performance of an Express application?
To improve performance:
- Use GZIP Compression: Employ
compressionmiddleware to reduce response body size. - Efficient Middleware Chain: Order middleware carefully; expensive operations should come later, or be conditional.
- Caching: Implement HTTP caching (ETags, Cache-Control headers) for static assets and API responses.
- Database Optimization: Optimize queries, use indexing, and connection pooling.
- Asynchronous Operations: Ensure database calls and I/O are non-blocking; use
async/await. - Logging: Use efficient loggers (e.g., Winston, Pino) and avoid excessive synchronous logging.
- Load Balancing: Distribute traffic across multiple instances.
- Avoid Blocking Code: Keep event loop unblocked.
22. Discuss strategies for scaling an Express application horizontally.
Scaling horizontally means running multiple instances of your Express application behind a load balancer. Each instance handles a subset of the incoming requests. Strategies include:
- Load Balancing: Use NGINX, HAProxy, or cloud load balancers (AWS ELB, GCP Load Balancer) to distribute requests across instances.
- Statelessness: Design applications to be stateless. Avoid storing session data directly on the server instance. Use external stores like Redis or Memcached for session management.
- Shared Resources: Centralize databases, caches (Redis), and message queues (RabbitMQ, Kafka) so all instances can access them.
- Containerization & Orchestration: Deploy applications in Docker containers and manage them with Kubernetes for easy scaling, deployment, and self-healing.
23. What is clustering in Node.js, and how does it relate to Express?
The Node.js cluster module allows a single Node.js process to fork into multiple worker processes. These worker processes share the same server port, effectively utilizing multiple CPU cores on a single machine. For an Express application, using the cluster module means you can run multiple instances of your Express app, each in its own worker process, managed by a master process. This is a form of vertical scaling to improve performance by leveraging multi-core processors, but it is typically still limited to a single physical server and doesn't provide fault tolerance like true horizontal scaling.
24. How do you implement rate limiting in Express?
Rate limiting prevents abuse by restricting the number of requests a user can make to an API endpoint within a given time frame. This is crucial for security against brute-force attacks and DDoS attacks. The popular express-rate-limit middleware is commonly used. You configure it with the maximum number of requests and the time window. It can identify clients by their IP address, or by tokens/headers.
const rateLimit = require('express-rate-limit');
const apiLimiter = 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('/api/', apiLimiter); // Apply to all /api/ routes
25. Explain the role of caching in an Express application.
Caching stores frequently accessed data or generated responses to reduce redundant computations, database queries, and network requests, thereby improving response times and reducing server load. In an Express application, caching can be applied at multiple levels:
- Client-side (Browser Cache): Using HTTP cache headers (
Cache-Control,ETag) for static assets and idempotent API responses. - Server-side (Application Cache): In-memory caches (e.g., Node-Cache, LRU-Cache) for frequently used data, or external distributed caches (e.g., Redis, Memcached) for shared state across multiple instances.
- Proxy Cache: Reverse proxies like NGINX can cache responses before they even reach your Express app.
Advanced Topics & Best Practices
26. Discuss the use of Promises/Async-Await with Express middleware.
Modern Express applications heavily leverage async/await for handling asynchronous operations, making code more readable and maintainable than traditional callbacks. When using async functions as middleware or route handlers, any unhandled promise rejections inside them will not automatically be caught by Express's error handler. You must explicitly catch errors and pass them to next(err), or use a wrapper utility like express-async-handler to automate this.
const asyncHandler = require('express-async-handler'); // npm i express-async-handler
app.get('/data', asyncHandler(async (req, res) => {
const result = await someAsyncOperation();
res.json(result);
}));
// Without asyncHandler, you'd do:
// app.get('/data', async (req, res, next) => {
// try {
// const result = await someAsyncOperation();
// res.json(result);
// } catch (error) {
// next(error); // Pass error to the error handler
// }
// });
27. How would you structure a large Express application?
For large Express applications, a modular and organized structure is crucial:
- Separate Routes: Use
express.Routerfor different resource categories (e.g.,routes/users.js,routes/products.js). - Middleware Directory: Place custom middleware in a
middleware/folder. - Controllers/Handlers: Isolate business logic into
controllers/orhandlers/files, keeping routes lean. - Services/Repositories: Abstract database interactions and complex business logic into
services/orrepositories/. - Configuration: Manage environment variables and settings in a
config/directory. - Models/Schemas: Define data structures and database models in a
models/directory. - Utilities: Generic helper functions in a
utils/folder. This separation enhances maintainability, testability, and scalability.
28. Explain dependency injection in the context of Express.
Dependency Injection (DI) is a design pattern where components receive their dependencies from an external source rather than creating them. In Express, this means passing dependencies (like database connections, services, or configuration objects) to your route handlers or controllers, rather than having them require() or instantiate them directly within the handler. This improves testability, modularity, and flexibility. For instance, you might pass a userService instance to a controller instead of having the controller manage its creation. While Express itself doesn't have a built-in DI container, libraries like awilix or simply passing arguments facilitate this pattern.
29. What are the common tools/libraries used with Express (e.g., ORMs, validation)?
Common tools and libraries enhancing Express development:
- ORMs/ODMs: Prisma, Sequelize (SQL), Mongoose (MongoDB) for database interaction.
- Validation: Joi, Yup, express-validator for schema validation of request bodies.
- Authentication: Passport.js for various authentication strategies (JWT, OAuth, local).
- Security: Helmet (HTTP headers), CORS (cross-origin resource sharing), csurf (CSRF protection).
- Logging: Winston, Pino for structured logging.
- Testing: Supertest (for HTTP assertions), Jest, Mocha/Chai for unit/integration tests.
- Configuration: dotenv, config for environment variables.
- API Documentation: Swagger/OpenAPI with
swagger-ui-express.
Scenario-Based & System Design
30. Design a simple RESTful API for a blogging platform. Describe routes, middleware, and data flow.
API Design: Blogging Platform
Entities: Post (id, title, content, author, createdAt, updatedAt), Comment (id, postId, author, content, createdAt).
Routes:
GET /posts: Retrieve all posts.GET /posts/:id: Retrieve a single post.POST /posts: Create a new post (requires authentication).PUT /posts/:id: Update a post (requires authentication, author check).DELETE /posts/:id: Delete a post (requires authentication, author check).GET /posts/:id/comments: Retrieve comments for a post.POST /posts/:id/comments: Add a comment to a post (requires authentication).
Middleware:
express.json(): For parsing JSON request bodies.authenticationMiddleware: Verifies JWT fromAuthorizationheader, populatesreq.user.authorizationMiddleware: Checksreq.user's permissions/ownership forPUT/DELETEon posts.validationMiddleware: Uses Joi to validatePOSTandPUTrequest bodies forPostandCommentschemas.errorHandlerMiddleware: Catches errors (404for not found,500for server errors, custom401/403).
Data Flow (e.g., POST /posts):
- Client sends
POST /postswith JSONbodyandAuthorizationheader. express.json()parses body.authenticationMiddlewareverifies token, attaches user info toreq.validationMiddlewarevalidatesreq.bodyagainstPostschema.- Route handler (e.g.,
postController.createPost) receives validated data, saves to database (e.g., using Prisma). - If successful, sends
201 Createdwith new post data. - If error (validation, DB error),
errorHandlerMiddlewareintercepts and sends appropriate4xx/5xxresponse.
Quick revision table
| Topic | One-line answer | Common follow-up |
|---|---|---|
| Middleware | Functions with (req, res, next) to process requests sequentially. | How do you handle errors in middleware? |
| Routing | Maps HTTP methods and paths to handler functions. | What's the difference between app.get and router.get? |
| Error Handling | Uses four-argument (err, req, res, next) middleware at the end of the stack. | How do you handle async errors in Express? |
| Security | Mitigated via Helmet, input sanitization, CSRF tokens, rate limiting, and robust auth. | Explain XSS vs. CSRF and how to prevent them. |
| Scalability | Achieved through horizontal scaling (load balancing), stateless design, and caching. | How does Node.js cluster module help with scaling? |
req / res Objects | req holds incoming request info; res provides methods to send the response. | How do you access path parameters and query parameters? |
| Static Files | Served using express.static middleware from a designated directory. | How would you serve files from multiple directories? |
| Async/Await | Modern JS for cleaner async operations, but requires proper error propagation via next(err). | How does express-async-handler simplify error handling with async/await? |
| Express Router | Modular, self-contained set of routes and middleware for organizing large applications. | When should you use a router instead of defining all routes directly on app? |
How to prepare
- Build Actual Projects: The best way to learn Express is by building several types of applications: a REST API, a simple web app with templating, and perhaps a WebSocket integration. This hands-on experience solidifies concepts.
- Understand Core Node.js Concepts: Express sits on Node.js. Ensure you understand the Node.js event loop, non-blocking I/O, Streams, and Promises/Async-Await, as these are fundamental to Express performance and error handling.
- Practice Error Handling Robustly: Many junior developers overlook comprehensive error handling. Focus on implementing global error handlers, distinguishing between synchronous/asynchronous errors, and providing meaningful client responses without leaking sensitive data.
- Dive into Middleware Ecosystem: Explore popular third-party middleware (e.g.,
helmet,cors,passport.js,express-rate-limit,joi). Understand their purpose, configuration, and how they integrate into the request-response cycle. - Think About Scalability and Security from Day 1: When designing solutions, consider how your Express app would scale under load and how you'd protect it against common web vulnerabilities. Discussing these aspects demonstrates a senior mindset.
