This document contains common questions and answers that will help you become a proficient Node.js and Express.js backend developer. These are questions you might encounter in interviews or need to understand deeply.
- Node.js Fundamentals
- Express.js Concepts
- Backend Development Best Practices
- API Design & REST
- Authentication & Security
- Error Handling & Debugging
- Performance & Optimization
- Database & Data Management
- Testing & Deployment
- Common Interview Questions
Answer: Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. It allows JavaScript to run on the server side, outside of the browser. It's used for backend development because:
- Single Language: Use JavaScript for both frontend and backend
- Non-blocking I/O: Asynchronous, event-driven architecture handles many concurrent connections efficiently
- Large Ecosystem: npm (Node Package Manager) has millions of packages
- Fast: V8 engine compiles JavaScript to machine code
- Scalable: Great for handling many simultaneous connections
Answer: The event loop is what allows Node.js to perform non-blocking I/O operations. It:
- Monitors the call stack and callback queue
- Moves callbacks from the queue to the call stack when the stack is empty
- Handles asynchronous operations efficiently
- Uses phases: timers, pending callbacks, idle/prepare, poll, check, close callbacks
Key Concept: Node.js is single-threaded, but the event loop allows it to handle concurrency through asynchronous operations.
Answer:
-
require()(CommonJS):- Synchronous loading
- Used in Node.js by default
- Returns module.exports
- Example:
const express = require('express')
-
import(ES6 Modules):- Asynchronous loading
- Must use
.mjsextension or"type": "module"in package.json - Static analysis (can't be conditional)
- Example:
import express from 'express'
Answer:
module.exports is the object that gets returned when you use require() to import a module. It allows you to:
- Export functions, objects, or values
- Make code reusable across files
- Organize code into modules
Examples:
// Export a single function
module.exports = function greet(name) { ... }
// Export multiple items
module.exports = {
function1,
function2,
variable
}
// Or using exports shortcut
exports.function1 = function() { ... }Answer:
-
Synchronous: Code executes line by line, blocking execution until each operation completes
- Example:
fs.readFileSync()- blocks until file is read
- Example:
-
Asynchronous: Code continues executing while waiting for operations to complete
- Example:
fs.readFile()- continues execution, calls callback when done - Uses callbacks, promises, or async/await
- Example:
Why it matters: Asynchronous code allows Node.js to handle many requests concurrently without blocking.
Answer:
-
Promises: Represent the eventual completion (or failure) of an asynchronous operation
- States: pending, fulfilled, rejected
- Methods:
.then(),.catch(),.finally()
-
async/await: Syntactic sugar for Promises
- Makes asynchronous code look synchronous
asyncfunctions return Promisesawaitpauses execution until Promise resolves
Example:
// Promise
fetchData()
.then((data) => console.log(data))
.catch((error) => console.error(error));
// async/await
async function getData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
}Answer: Express.js is a minimal, flexible Node.js web application framework that provides:
- Routing: Easy URL routing and handling
- Middleware: Functions that execute during request/response cycle
- HTTP Helpers: Simplified request/response handling
- Template Engines: Support for rendering views
- Simplified Syntax: Much easier than raw Node.js HTTP module
Why use it: It dramatically reduces boilerplate code and makes building APIs much faster and cleaner.
Answer: Middleware functions have access to:
- Request object (
req) - Response object (
res) - Next function (
next)
They can:
- Execute code
- Modify request/response objects
- End the request-response cycle
- Call the next middleware
Types:
- Application-level:
app.use() - Router-level:
router.use() - Error-handling: 4 parameters
(err, req, res, next) - Built-in:
express.json(),express.static() - Third-party:
cors,helmet,morgan
Answer: Middleware executes in the order it's defined:
- Application-level middleware (in order defined)
- Route-specific middleware
- Route handler
- Error-handling middleware (if error occurs)
Important: Middleware order matters! For example, express.json() must come before routes that need req.body.
Answer:
-
app.get(): Handles GET requests for a specific route- Example:
app.get('/users', handler)- only handles GET /users
- Example:
-
app.use(): Applies middleware to all HTTP methods and routes (matching or below)- Example:
app.use('/api', middleware)- applies to all methods on /api and sub-routes
- Example:
Answer: Express provides methods for each HTTP verb:
app.get()- GET requestsapp.post()- POST requestsapp.put()- PUT requestsapp.delete()- DELETE requestsapp.patch()- PATCH requestsapp.all()- All HTTP methods
Example:
app.get("/users", getUsers);
app.post("/users", createUser);
app.put("/users/:id", updateUser);
app.delete("/users/:id", deleteUser);Answer: Express Router is a mini Express application that provides routing functionality. Use it to:
- Organize routes into separate files
- Create modular route handlers
- Apply middleware to specific route groups
- Improve code organization and maintainability
Example:
// routes/users.js
const router = express.Router();
router.get("/", getUsers);
router.post("/", createUser);
module.exports = router;
// app.js
app.use("/api/users", require("./routes/users"));Answer: Separating code into logical layers:
- Routes: Handle HTTP requests/responses, define endpoints
- Controllers: Business logic, process requests
- Models/Data: Data access layer, database operations
- Services: Reusable business logic
- Middleware: Cross-cutting concerns (auth, validation, logging)
Benefits:
- Easier to maintain
- Easier to test
- Easier to scale
- Better code organization
Answer: Recommended structure:
project/
├── src/
│ ├── routes/ # Route definitions
│ ├── controllers/ # Business logic
│ ├── models/ # Data models
│ ├── middleware/ # Custom middleware
│ ├── services/ # Business services
│ ├── utils/ # Helper functions
│ ├── config/ # Configuration
│ └── app.js # Express app setup
├── tests/ # Test files
├── package.json
└── .env # Environment variables
Answer: Environment variables store configuration outside your code:
- Security: Keep sensitive data (API keys, passwords) out of code
- Flexibility: Different configs for dev/staging/production
- Portability: Easy to change settings without code changes
Usage:
require("dotenv").config();
const PORT = process.env.PORT || 3000;
const DB_URL = process.env.DATABASE_URL;Never commit .env files to git!
Answer: Best Practices:
- Try-catch blocks in async functions
- Error-handling middleware (4 parameters:
err, req, res, next) - Consistent error format across the application
- Appropriate HTTP status codes
- Error logging for debugging
- Don't expose internal errors to clients in production
Example:
// Error middleware (must be last)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || "Internal Server Error",
});
});Answer: Input validation checks if data meets requirements before processing:
- Security: Prevents injection attacks, XSS
- Data Integrity: Ensures data is in correct format
- User Experience: Provides clear error messages
- System Stability: Prevents crashes from invalid data
Methods:
- Manual validation (checking fields)
- Libraries:
joi,express-validator,yup - Validation middleware
Always validate:
- Required fields exist
- Data types are correct
- Values meet constraints (e.g., positive numbers)
- Format is valid (e.g., email format)
Answer: REST (Representational State Transfer) is an architectural style for APIs:
Principles:
- Stateless: Each request contains all needed information
- Client-Server: Clear separation of concerns
- Uniform Interface: Consistent URL structure and HTTP methods
- Resource-Based: URLs represent resources, not actions
- HTTP Methods: Use GET, POST, PUT, DELETE appropriately
- Status Codes: Use proper HTTP status codes
RESTful URLs:
GET /api/users- Get all usersGET /api/users/:id- Get user by IDPOST /api/users- Create userPUT /api/users/:id- Update userDELETE /api/users/:id- Delete user
Answer: Success (2xx):
200 OK- Successful GET, PUT, PATCH201 Created- Successful POST (resource created)204 No Content- Successful DELETE
Client Error (4xx):
400 Bad Request- Invalid request data401 Unauthorized- Authentication required403 Forbidden- Authorized but not permitted404 Not Found- Resource doesn't exist409 Conflict- Resource conflict (e.g., duplicate)
Server Error (5xx):
500 Internal Server Error- Server error
Answer: Success Response:
{
"success": true,
"data": { ... },
"message": "Optional message"
}Error Response:
{
"success": false,
"error": "Error message",
"details": "Additional details"
}Benefits:
- Frontend can easily check
successfield - Consistent structure across all endpoints
- Better error handling on client side
Answer: Pagination limits the number of results returned per request:
- Performance: Prevents loading too much data
- User Experience: Faster page loads
- Resource Management: Reduces server load
Implementation:
// Query parameters
GET /api/products?page=1&limit=10
// Response
{
"data": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 100,
"totalPages": 10
}
}Answer:
-
Authentication: Verifying WHO the user is (login)
- "Are you really John?"
- Usually done with username/password, tokens
-
Authorization: Verifying WHAT the user can do (permissions)
- "Can John access this resource?"
- Based on roles, permissions, ownership
Example:
- User logs in (authentication) → Gets token
- User tries to access
/api/admin/users(authorization) → Check if user is admin
Answer: JWT is a token-based authentication method:
Process:
- User logs in with credentials
- Server validates credentials
- Server creates JWT token (contains user info)
- Server sends token to client
- Client stores token (localStorage, cookies)
- Client sends token in Authorization header for protected requests
- Server validates token and extracts user info
Token Structure:
- Header: Algorithm and token type
- Payload: User data (user ID, role, etc.)
- Signature: Verifies token hasn't been tampered with
Benefits:
- Stateless (no server-side session storage)
- Scalable
- Can include user info in token
Answer: Security Risks:
- If database is breached, all passwords are exposed
- Users often reuse passwords across sites
- Legal and compliance issues
Solution:
- Hash passwords using libraries like
bcrypt - Use salt (random data added before hashing)
- Never return password hashes in API responses
Example:
const bcrypt = require("bcrypt");
const saltRounds = 10;
// Hash password
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Verify password
const isValid = await bcrypt.compare(password, hashedPassword);Answer: CORS (Cross-Origin Resource Sharing) allows browsers to make requests to different origins (domain, protocol, or port).
Why needed:
- Browsers block cross-origin requests by default (same-origin policy)
- Your frontend (localhost:3000) needs to call API (localhost:5000)
- Different domains need to communicate
Solution:
const cors = require("cors");
app.use(cors()); // Allow all origins (development)
// Production: specify allowed origins
app.use(
cors({
origin: "https://yourdomain.com",
})
);Answer:
- HTTPS: Always use HTTPS in production
- Input Validation: Validate and sanitize all inputs
- Password Hashing: Never store plain text passwords
- Environment Variables: Keep secrets in .env files
- Rate Limiting: Prevent brute force attacks
- Helmet.js: Set security HTTP headers
- SQL Injection Prevention: Use parameterized queries
- XSS Prevention: Sanitize user inputs
- CORS Configuration: Restrict allowed origins
- Error Handling: Don't expose internal errors
Answer: Methods:
- Console.log(): Simple logging (remove in production)
- Debugger: Use
node --inspector VS Code debugger - Logging Libraries: Winston, Pino, Morgan
- Error Tracking: Sentry, Rollbar
- Network Tools: Postman, curl, browser DevTools
Best Practices:
- Log errors with context
- Use log levels (info, warn, error)
- Don't log sensitive data
- Use structured logging
Answer:
-
throw error: Throws error in current execution context- Must be caught with try-catch
- Works in async functions
-
next(error): Passes error to Express error-handling middleware- Designed for Express error handling
- Automatically goes to error middleware
Best Practice: Use next(error) in Express route handlers to properly trigger error-handling middleware.
Answer: Problem: Errors in async functions don't automatically go to error middleware.
Solutions:
- Wrap in try-catch:
app.get("/users", async (req, res, next) => {
try {
const users = await getUsers();
res.json(users);
} catch (error) {
next(error); // Pass to error middleware
}
});- Use wrapper function:
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get(
"/users",
asyncHandler(async (req, res) => {
const users = await getUsers();
res.json(users);
})
);Answer: Strategies:
- Caching: Cache frequently accessed data (Redis, in-memory)
- Database Optimization: Use indexes, optimize queries
- Compression: Use
compressionmiddleware - Connection Pooling: Reuse database connections
- Load Balancing: Distribute requests across servers
- CDN: Serve static files from CDN
- Code Optimization: Avoid blocking operations
- Async Operations: Use async/await properly
- Monitoring: Track performance metrics
Answer:
-
Blocking: Code stops execution until operation completes
- Example:
fs.readFileSync()- blocks until file is read - Blocks entire event loop
- Example:
-
Non-blocking: Code continues while waiting for operation
- Example:
fs.readFile()- continues, calls callback when done - Doesn't block event loop
- Example:
Rule: Always prefer non-blocking operations in Node.js to maintain performance.
Answer:
Compression middleware (like compression) reduces response size by compressing data:
- Benefits: Faster response times, less bandwidth
- Usage:
app.use(compression()) - Supports: gzip, deflate algorithms
Result: Text responses (JSON, HTML) are compressed, reducing size by 70-90%.
Answer: SQL (Relational):
- Structured data in tables
- Relationships between tables
- ACID compliance
- Examples: PostgreSQL, MySQL
- Use when: Need complex queries, relationships, transactions
NoSQL:
- Flexible schema (document, key-value, graph)
- Horizontal scaling
- Examples: MongoDB, Redis
- Use when: Need flexibility, scalability, simple structure
Answer:
- ORM (Object-Relational Mapping): Maps database tables to JavaScript objects
- ODM (Object-Document Mapping): Maps documents to JavaScript objects
Examples:
- Sequelize: ORM for SQL databases
- Mongoose: ODM for MongoDB
- TypeORM: TypeScript ORM
Benefits:
- Easier database queries (JavaScript instead of SQL)
- Database abstraction
- Migrations and schema management
- Validation and relationships
Answer: Connection pooling reuses database connections instead of creating new ones for each request:
- Benefits: Better performance, efficient resource usage
- Implementation: Most database drivers support it automatically
- Configuration: Set pool size based on application needs
Answer: Benefits:
- Catch Bugs Early: Find issues before production
- Documentation: Tests show how code should work
- Refactoring Safety: Ensure changes don't break functionality
- Confidence: Deploy with assurance
Types:
- Unit Tests: Test individual functions
- Integration Tests: Test API endpoints
- End-to-End Tests: Test complete flows
Answer: Tools:
- Jest: Test framework
- Supertest: HTTP assertions for testing APIs
- Mocha/Chai: Alternative testing framework
Example:
const request = require("supertest");
const app = require("../app");
test("GET /api/users", async () => {
const response = await request(app).get("/api/users").expect(200);
expect(response.body.success).toBe(true);
});Answer: Different settings for different environments:
Development:
- Detailed error messages
- Debug logging
- Local database
- CORS allows localhost
Production:
- Generic error messages
- Minimal logging
- Production database
- CORS restricted to specific domains
- HTTPS enabled
Implementation:
const env = process.env.NODE_ENV || "development";
if (env === "production") {
// Production config
} else {
// Development config
}Answer: Node.js uses an event-driven, non-blocking I/O model:
- Event Loop: Monitors call stack and callback queue
- Single Thread: Main thread handles all operations
- Non-blocking: I/O operations don't block execution
- Callbacks: Functions executed when operations complete
- Event Emitters: Objects that emit events (like HTTP requests)
Benefits:
- Handles many concurrent connections efficiently
- No need for thread management
- Fast and scalable
Answer:
- Node.js checks if module is cached
- If not cached, reads and executes the file
- Wraps code in a function with
exports,require,module,__filename,__dirname - Executes the wrapped code
- Caches the module
- Returns
module.exports
Key Point: Modules are cached after first require, so subsequent requires return the cached version.
Answer:
Use middleware like multer:
const multer = require("multer");
const upload = multer({ dest: "uploads/" });
app.post("/upload", upload.single("file"), (req, res) => {
// req.file contains file information
res.json({ file: req.file });
});Considerations:
- File size limits
- File type validation
- Storage location (local or cloud)
- Security (scan for malware)
Answer:
-
req.params: Route parameters (from URL path)- Example:
/users/:id→req.params.id
- Example:
-
req.query: Query string parameters- Example:
/users?page=1&limit=10→req.query.page,req.query.limit
- Example:
-
req.body: Request body data (POST, PUT)- Requires
express.json()middleware - Contains JSON/form data
- Requires
Answer:
Use express-rate-limit middleware:
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
app.use("/api/", limiter);Benefits:
- Prevents brute force attacks
- Protects against DDoS
- Controls API usage
Answer:
-
PUT: Replaces entire resource
- Must send complete resource data
- Idempotent (same request = same result)
-
PATCH: Partially updates resource
- Only send fields to update
- Not necessarily idempotent
Example:
// PUT - replace entire user
PUT /users/1
{ "name": "John", "email": "john@example.com", "age": 30 }
// PATCH - update only name
PATCH /users/1
{ "name": "Jane" }Answer: Transactions ensure multiple operations succeed or fail together:
With Sequelize (SQL):
const transaction = await sequelize.transaction();
try {
await User.create({...}, { transaction });
await Order.create({...}, { transaction });
await transaction.commit();
} catch (error) {
await transaction.rollback();
}Benefits:
- Data consistency
- Atomic operations
- Rollback on errors
Answer:
process.env is an object containing environment variables:
- Set by operating system or
.envfile (with dotenv) - Used for configuration (port, database URLs, API keys)
- Different values for different environments
- Access:
process.env.PORT
Best Practice: Always provide default values:
const PORT = process.env.PORT || 3000;Answer: Query Parameters:
// GET /api/products?page=1&limit=10
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const products = await Product.find().skip(skip).limit(limit);
const total = await Product.countDocuments();
res.json({
data: products,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});Answer:
-
process.nextTick(): Executes before any other async operation- Highest priority in event loop
- Can starve event loop if used excessively
-
setImmediate(): Executes in next iteration of event loop- Lower priority than
nextTick - Better for I/O operations
- Lower priority than
Rule: Use setImmediate() unless you need the highest priority (rare).
Answer: Basic Search:
// GET /api/products?search=laptop
const search = req.query.search || "";
const products = await Product.find({
$or: [
{ name: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
],
});Advanced:
- Use full-text search indexes
- Implement fuzzy search
- Search across multiple fields
- Consider search libraries (Elasticsearch)
Answer:
- Resource-Based URLs:
/usersnot/getUsers - HTTP Methods: Use GET, POST, PUT, DELETE correctly
- Stateless: Each request contains all needed info
- Status Codes: Use appropriate HTTP status codes
- JSON Format: Use JSON for data exchange
- Versioning:
/api/v1/usersfor API versioning - Filtering/Sorting: Use query parameters
- Consistent Naming: Use plural nouns for resources
- Practice Regularly: Build projects, not just read
- Understand Concepts: Don't just memorize, understand why
- Read Documentation: Official docs are your best friend
- Code Review: Review your own and others' code
- Build Projects: Apply what you learn in real projects
- Debug Actively: Learn to debug effectively
- Stay Updated: Follow Node.js and Express updates
Remember: Understanding these concepts deeply is more important than memorizing answers. Practice building applications and solving real problems to become a proficient backend developer! 🚀