This repository contains an Express.js application with detailed examples and explanations for beginners. The codebase demonstrates how to build a RESTful API with Express.js, covering essential concepts with practical implementation.
- Project Overview
- Project Structure
- Setup and Installation
- Core Concepts Explained
- Refactoring for Better Organization
- API Endpoints
- Testing the API
- Dependencies
This project is a learning resource for Express.js, demonstrating how to create a simple REST API with mock user data. It includes examples of:
- Setting up an Express server
- Creating and using middleware
- Implementing various HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Validating request data
- Handling route and query parameters
- Organizing routes with Express Router
- Working with HTTP cookies for state management
- Managing user sessions with express-session
- Implementing user authentication with Passport.js
express-tut/
├── src/
│ ├── index.mjs # Main application file (clean router implementation)
│ ├── oldindex.mjs # Original implementation before refactoring
│ ├── strategies/
│ │ └── local-strategy.mjs # Passport.js local authentication strategy
│ └── routes/
│ ├── index.mjs # Combined router module
│ ├── users.mjs # User routes module
│ └── products.mjs # Product routes module
├── utils/
│ ├── constants.mjs # Mock data and constants (includes mock users)
│ ├── middlewares.mjs # Middleware functions
│ └── validationSchemas.mjs # Data validation schemas
├── package.json # Project dependencies and scripts
└── README.md # Project documentation
-
Clone the repository:
git clone https://github.com/your-username/express-tut.git cd express-tut
-
Install dependencies:
npm install
-
Run the development server:
npm run start:dev
This uses nodemon to automatically restart the server when files change.
-
Run in production mode:
npm start
// Basic Express server setup
import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Our application starts by importing Express and creating an instance of it. We define a port (either from environment variables or defaulting to 3000) and start the server with app.listen()
.
Middleware functions are functions that have access to the request object, response object, and the next middleware function. They can:
- Execute code
- Make changes to the request/response objects
- End the request-response cycle
- Call the next middleware
// Middleware for parsing JSON in request bodies
app.use(express.json());
This middleware parses incoming JSON requests and puts the data in request.body
.
// Custom logging middleware
const loggingMiddleware = (request, response, next) => {
console.log(`${request.method} request to ${request.url}`);
next(); // Call the next middleware or route handler
};
// Apply middleware globally
app.use(loggingMiddleware);
// Or apply to specific routes
app.get('/path', loggingMiddleware, (request, response) => {
// Route handler
});
Our loggingMiddleware
logs the HTTP method and URL for each request, then passes control to the next function with next()
.
app.get('/',
(request, response, next) => {
console.log(`url 1`);
next();
},
(request, response, next) => {
console.log(`url 2`);
next();
},
(request, response) => {
response.status(201).send({msg: "Hello World!"});
}
);
This demonstrates how multiple middleware functions can be chained on a single route, executing in sequence.
const resolveIndexById = (request, response, next) => {
const {params: {id}} = request;
const parsedId = parseInt(id);
if(isNaN(parsedId))
return response.status(400).send({msg: "Invalid user ID, Bad Request"});
const findUser = mockUsers.findIndex(user => user.id === parsedId);
if(findUser === -1)
return response.status(404).send({msg: "User not found"});
request.findUser = findUser;
next();
};
This middleware validates and processes ID parameters, attaching the user index to the request object for use in route handlers.
Express routes determine how the application responds to client requests to specific endpoints (URLs) and HTTP methods.
app.get('/', (request, response) => {
response.status(201).send({msg: "Hello World!"});
});
This defines a route that responds to GET requests at the root path with a "Hello World!" JSON message.
Our application implements all standard HTTP methods for a RESTful API:
// Get all users
app.get('/api/users', (request, response) => {
return response.send(mockUsers);
});
// Get a specific user by ID
app.get('/api/users/:id', resolveIndexById, (request, response) => {
const {findUser} = request;
const findUserIndex = mockUsers[findUser];
response.send(findUserIndex);
});
// Create a new user
app.post('/api/users', checkSchema(createUserValidationSchema), (request, response) => {
const result = validationResult(request);
if (!result.isEmpty())
return response.status(400).send({ errors: result.array() });
const data = matchedData(request);
const newUser = {id: mockUsers[mockUsers.length - 1].id + 1, ...data};
mockUsers.push(newUser);
return response.status(201).send(newUser);
});
// Replace all user data
app.put('/api/users/:id', resolveIndexById, (request, response) => {
const {body, findUser} = request;
mockUsers[findUser] = { id: mockUsers[findUser].id, ...body};
return response.status(200).send(mockUsers[findUser]);
});
// Update specific user fields
app.patch('/api/users/:id', resolveIndexById, (request, response) => {
const {body, findUser} = request;
mockUsers[findUser] = { ...mockUsers[findUser], ...body};
return response.status(200).send(mockUsers[findUser]);
});
// Delete a user
app.delete('/api/users/:id', resolveIndexById, (request, response) => {
const {findUser} = request;
mockUsers.splice(findUser, 1);
return response.status(204).send();
});
We use express-validator
to validate request data:
import { query, validationResult, body, matchedData, checkSchema } from 'express-validator';
// Validate query parameters
app.get('/api/users',
query('filter')
.isString()
.notEmpty()
.withMessage('Filter must be a non-empty string')
.isLength({ min: 3, max: 10 })
.withMessage('Filter must be a string with length between 3 and 10 characters'),
(request, response) => {
const result = validationResult(request);
// Handle validation results
}
);
// Validate request body using a schema
app.post('/api/users',
checkSchema(createUserValidationSchema),
(request, response) => {
const result = validationResult(request);
if (!result.isEmpty()) {
return response.status(400).send({ errors: result.array() });
}
// Process validated data
const data = matchedData(request);
// ...
}
);
Our validation schema for user creation:
// utils/validationSchemas.mjs
export const createUserValidationSchema = {
name: {
isLength: {
options: { min: 5, max: 32 },
errorMessage: 'Name must be between 1 and 32 characters long.',
},
notEmpty: {
errorMessage: 'Name cannot be empty.',
},
isString: {
errorMessage: 'Name must be a string.',
},
},
displayName: {
isLength: {
options: { min: 3, max: 32 },
errorMessage: 'Display name must be between 1 and 32 characters long.',
},
notEmpty: {
errorMessage: 'Display name cannot be empty.',
},
isString: {
errorMessage: 'Display name must be a string.',
},
},
};
Route parameters are named URL segments used to capture values at specific positions in the URL:
// :id is a route parameter
app.get('/api/users/:id', resolveIndexById, (request, response) => {
// Access the parameter via request.params.id
const {findUser} = request;
const findUserIndex = mockUsers[findUser];
response.send(findUserIndex);
});
In this example, :id
in the route path captures any value at that position in the URL.
Query parameters are key-value pairs appended to the URL after a question mark:
app.get('/api/users', (request, response) => {
const { query: { filter, value } } = request;
if (filter && value) {
return response.send(
mockUsers.filter(user => user[filter].includes(value))
);
}
return response.send(mockUsers);
});
Example URL with query parameters: /api/users?filter=name&value=J
Express Routers allow you to modularize your routes and create reusable route handlers. This is particularly useful as your application grows, helping you organize routes by resource or functionality.
// src/routes/users.mjs
import { Router } from "express";
import { query, validationResult } from "express-validator";
import { mockUsers } from "../../utils/constants.mjs";
const router = Router();
// Define routes on the router
router.get('/api/users', query('filter')
.isString()
.notEmpty()
.withMessage('Filter must be a non-empty string')
.isLength({ min: 3, max: 10 })
.withMessage('Filter must be a string with length between 3 and 10 characters'),
(request, response) => {
// Route handler logic
const result = validationResult(request);
const { query: { filter, value } } = request;
if (filter && value) {
// Filter users based on query parameters
}
return response.send(mockUsers);
});
export default router;
The project organizes routes by resource type:
// src/routes/products.mjs
import { Router } from "express";
const router = Router();
router.get('/api/products', (request, response) => {
response.send([{id:123, name: 'GTA VI'},
{id:456, name: 'RDR3'}]);
});
export default router;
For better organization, we can import and use multiple routers in the main application:
// src/routes/index.mjs
import { Router } from "express";
import usersRouter from "./users.mjs";
import productsRouter from "./products.mjs";
const router = Router();
// Combine all routers into a single router
router.use(usersRouter);
router.use(productsRouter);
export default router;
// src/index.mjs
import express from 'express';
import router from './routes/index.mjs';
const app = express();
app.use(express.json());
// Use the combined router for all routes
app.use(router);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
This approach offers several benefits:
- Separation of concerns: Each resource has its own dedicated file
- Code organization: Routes are grouped logically by function
- Maintainability: Easier to locate and modify specific routes
- Scalability: New resources can be added without cluttering the main file
- Reusability: Router modules can be reused across different applications
HTTP is stateless by default, meaning each request is independent of previous ones. Cookies help maintain state by storing data on the client side that the server can access in future requests.
To work with cookies in Express, we use the cookie-parser
middleware:
// Install the dependency
// npm install cookie-parser
// Import the middleware
import cookieParser from 'cookie-parser';
// Use the middleware
const app = express();
app.use(cookieParser()); // Basic usage
// For signed cookies (more secure)
// app.use(cookieParser('mySecret')); // Pass a secret to sign cookies
The cookie-parser
middleware parses the Cookie header in incoming requests and populates request.cookies
with an object containing the cookie values.
Cookies are set in the response using the response.cookie()
method:
// Set a basic cookie that expires in 1 day
app.get('/', (request, response) => {
response.cookie('sessionId', '12345', {
maxAge: 1000 * 60 * 60 * 24 // 1 day in milliseconds
});
response.status(201).send({msg: "Hello World!"});
});
Cookie options include:
maxAge
: Time in milliseconds until the cookie expiresexpires
: Specific date when the cookie expireshttpOnly
: Makes the cookie inaccessible to client-side JavaScriptsecure
: Cookie only sent over HTTPSsigned
: Signs the cookie for tampering detection (requires secret in cookie-parser)
Once set, cookies are sent with every subsequent request to the same domain and can be accessed via request.cookies
:
router.get('/api/products', (request, response) => {
console.log(request.cookies); // Log all cookies
// Access a specific cookie
if (request.cookies.sessionId && request.cookies.sessionId === '12345') {
return response.send([{id:123, name: 'GTA VI'},
{id:456, name: 'RDR3'}]);
} else {
return response.status(403).send({msg: "You need correct cookie"});
}
});
Our project demonstrates a simple cookie-based authentication mechanism:
-
When a user visits the root route (
/
), we set a cookie:app.get('/', (request, response) => { response.cookie('sessionId', '12345', { maxAge: 1000 * 60 * 60 * 24 // 1 day }); response.status(201).send({msg: "Hello World!"}); });
-
Protected routes check for this cookie before providing access:
router.get('/api/products', (request, response) => { if (request.cookies.sessionId && request.cookies.sessionId === '12345') { // Authorized access return response.send([{id:123, name: 'GTA VI'}, {id:456, name: 'RDR3'}]); } else { // Unauthorized access return response.status(403).send({msg: "You need correct cookie"}); } });
This pattern forms the basis of more complex authentication systems, where the cookie might store a session ID that corresponds to more detailed user information stored on the server.
Sessions build upon cookies to provide server-side state management. While cookies store data on the client side, sessions store data on the server side and use a session ID cookie to link clients to their server-side data.
Our application uses the express-session
middleware to handle sessions:
// Import express-session
import session from 'express-session';
// Configure and apply the session middleware
app.use(session({
secret: 'zero',
saveUninitialized: false, // Don't save unmodified sessions (saves memory)
resave: false, // Don't save session if not modified
cookie: {
maxAge: 1000 * 60 * 60 * 24 // 1 day
}
}));
The configuration options:
secret
: Used to sign the session ID cookiesaveUninitialized
: When false, prevents storing empty sessions, reducing server storageresave
: When false, prevents saving the session if it wasn't modifiedcookie
: Options for the session ID cookie, including its lifespan
Aspect | Cookies | Sessions |
---|---|---|
Storage | Client-side (browser) | Server-side |
Security | Less secure (data stored on client) | More secure (only session ID stored on client) |
Size Limit | ~4KB | Limited only by server memory/storage |
Lifespan | Set by expiration time | Managed on server, can expire with inactivity |
Use Cases | Remembering preferences, tracking | User authentication, shopping carts, stateful apps |
The session object is attached to the request object and can be accessed and modified:
// Reading session data
app.get('/', (request, response) => {
console.log(request.session); // The entire session object
console.log(request.sessionID); // The session ID
// Modify session data
request.session.visited = true;
response.send({ msg: "Hello World!" });
});
Our application demonstrates a basic authentication system using sessions:
// Login route
app.post('/api/auth', (request, response) => {
const { body: { name, password } } = request;
// Validate credentials
const findUser = mockUsers.find(user => user.name === name);
if (!findUser || findUser.password !== password) {
return response.status(401).send({ msg: 'Invalid credentials' });
}
// Store user in session
request.session.user = findUser;
return response.status(200).send({
msg: 'User authenticated successfully',
user: {
id: findUser.id,
name: findUser.name,
displayName: findUser.displayName
}
});
});
// Check authentication status
app.get('/api/auth/status', (request, response) => {
if (request.session.user) {
return response.status(200).send({
msg: 'User is authenticated',
user: request.session.user
});
}
return response.status(401).send({ msg: 'User is not authenticated' });
});
The session middleware provides access to the session store, which can be useful for debugging or advanced use cases:
app.get('/api/auth/status', (request, response) => {
request.sessionStore.get(request.session.id, (err, session) => {
if (err) {
console.error('Error retrieving session:', err);
return response.status(500).send({ msg: 'Internal server error' });
}
console.log('Session data:', session);
});
// Rest of handler...
});
Sessions are perfect for features like shopping carts, where state needs to be maintained across requests:
// Add item to cart
app.post('/api/cart', (request, response) => {
// Require authentication
if (!request.session.user) return response.sendStatus(401);
const { body: item } = request;
const { cart } = request.session;
// Add to existing cart or create new cart
if (cart) {
cart.push(item);
} else {
request.session.cart = [item];
}
return response.status(201).send(item);
});
// Get cart contents
app.get('/api/cart', (request, response) => {
if (!request.session.user) return response.sendStatus(401);
return response.send(request.session.cart ?? []);
});
- Session Hijacking: If a malicious actor obtains a session ID, they can impersonate the user. Mitigate with HTTPS and secure cookies.
- Session Fixation: Regenerate session IDs after authentication to prevent attackers from setting known session IDs.
- Timeout: Set appropriate session timeouts to limit the window of vulnerability.
- Session Store: For production, use a dedicated session store (Redis, MongoDB, etc.) rather than the default memory store.
For production applications, consider using a dedicated session store:
// Example using connect-redis (requires additional setup)
import session from 'express-session';
import RedisStore from 'connect-redis';
app.use(session({
store: new RedisStore(options),
secret: 'zero',
resave: false,
saveUninitialized: false
}));
Popular session store options include:
connect-redis
: Redis-based session storeconnect-mongo
: MongoDB-based session storesession-file-store
: File system-based session store
Each client has a unique session ID stored in a cookie, allowing the server to identify the client and access their session data for subsequent requests. This stateful approach enables features that would be impossible with cookies alone, particularly when dealing with sensitive data or complex state management.
The codebase has evolved from a monolithic approach (all routes in one file) to a modular structure. Here's how the code was refactored:
In the original implementation, all routes and middleware were defined in a single file:
// All routes in one file
app.get('/api/users', (request, response) => { /* ... */ });
app.get('/api/users/:id', (request, response) => { /* ... */ });
app.post('/api/users', (request, response) => { /* ... */ });
app.put('/api/users/:id', (request, response) => { /* ... */ });
app.patch('/api/users/:id', (request, response) => { /* ... */ });
app.delete('/api/users/:id', (request, response) => { /* ... */ });
app.get('/api/products', (request, response) => { /* ... */ });
Common middleware functions were moved to a dedicated file:
// utils/middlewares.mjs
export const loggingMiddleware = (request, response, next) => {
console.log(`${request.method} request to ${request.url}`);
next();
};
export const resolveIndexById = (request, response, next) => {
// ID validation logic
// ...
next();
};
Routes were organized by resource type:
// routes/users.mjs
const router = Router();
router.get('/api/users', /* ... */);
router.post('/api/users', /* ... */);
router.get('/api/users/:id', /* ... */);
router.put('/api/users/:id', /* ... */);
router.patch('/api/users/:id', /* ... */);
router.delete('/api/users/:id', /* ... */);
export default router;
// routes/products.mjs
const router = Router();
router.get('/api/products', /* ... */);
export default router;
A central router file combines all resource routers:
// routes/index.mjs
import { Router } from "express";
import usersRouter from "./users.mjs";
import productsRouter from "./products.mjs";
const router = Router();
router.use(usersRouter);
router.use(productsRouter);
export default router;
The main application file is now simplified:
// index.mjs
import express from 'express';
import router from './routes/index.mjs';
const app = express();
app.use(express.json());
app.use(router);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
This refactoring demonstrates how a growing Express application can be organized to maintain code quality and scalability.
Method | Endpoint | Description | Location | Notes |
---|---|---|---|---|
GET | / | Returns a "Hello World" message and sets a cookie | index.mjs | Sets sessionId cookie |
GET | /api/users | Returns all users (can be filtered) | routes/users.mjs | |
GET | /api/users/:id | Returns a specific user by ID | routes/users.mjs | |
POST | /api/users | Creates a new user | routes/users.mjs | |
PUT | /api/users/:id | Completely updates a user | routes/users.mjs | |
PATCH | /api/users/:id | Partially updates a user | routes/users.mjs | |
DELETE | /api/users/:id | Deletes a user | routes/users.mjs | |
GET | /api/products | Returns a list of products | routes/products.mjs | Requires sessionId cookie |
POST | /api/auth | Authenticates a user | index.mjs | Uses Passport local strategy |
GET | /api/auth/status | Returns authentication status | index.mjs | Returns user data if authenticated |
POST | /api/auth/logout | Logs out the current user | index.mjs | Terminates the session |
POST | /api/cart | Adds an item to the user's cart | index.mjs | Requires authentication |
GET | /api/cart | Returns the user's cart | index.mjs | Requires authentication |
- express: Web framework for Node.js
- express-validator: Middleware for validating request data
- cookie-parser: Middleware for parsing cookies from request headers
- nodemon (dev): Utility for auto-restarting the server during development
- express-session: Middleware for managing user sessions
- passport: Authentication middleware
- passport-local: Local authentication strategy for Passport
To install dependencies:
npm install
This Express.js tutorial project demonstrates fundamental concepts for building RESTful APIs. The codebase is heavily commented to explain each concept clearly, making it perfect for beginners learning Express.js.
Our application implements user authentication using Passport.js, a popular authentication middleware for Node.js. Passport simplifies the process of handling authentication by providing a standardized way to authenticate requests.
// Install the dependencies
// npm install passport passport-local
// Import passport and the local strategy
import passport from "passport";
import { Strategy } from "passport-local";
import { mockUsers } from "../utils/constants.mjs";
// Initialize passport middleware after session setup
app.use(session({
secret: 'zero',
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 // 1 day
}
}));
app.use(passport.initialize());
app.use(passport.session());
We implement a local authentication strategy that validates username/password credentials against our mock user database:
// src/strategies/local-strategy.mjs
passport.use(new Strategy({
usernameField: 'name', // Use 'name' field from request body as username
passwordField: 'password' // Use 'password' field from request body
}, (name, password, done) => {
console.log(`name: ${name}, password: ${password}`);
try {
// Find user in our mock database
const findUser = mockUsers.find(user => user.name === name);
if (!findUser) throw new Error('User not found');
if (findUser.password !== password) throw new Error('Invalid password');
// Authentication successful - pass user object to next middleware
done(null, findUser);
}
catch (err) {
// Authentication failed - pass error to next middleware
done(err, null);
}
}));
For persistent authentication across requests, Passport needs to serialize and deserialize user objects to and from the session:
// Serialize: Store user ID in session
passport.serializeUser((user, done) => {
console.log('Serializing user:', user);
done(null, user.id); // Only store user.id in the session
});
// Deserialize: Retrieve full user object using ID from session
passport.deserializeUser((id, done) => {
console.log(`Deserializing user with id: ${id}`);
try {
// Find user by ID in our mock database
const findUser = mockUsers.find(user => user.id === id);
if (!findUser) throw new Error('User not found');
// Pass full user object to request.user
done(null, findUser);
} catch(err) {
done(err, null);
}
});
-
Login Endpoint:
app.post('/api/auth', passport.authenticate("local"), (request, response) => { // If this function is called, authentication was successful // request.user contains the authenticated user response.status(200).send({ message: 'Authentication successful', user: { id: request.user.id, name: request.user.name, displayName: request.user.displayName } }); });
-
Authentication Status Endpoint:
app.get('/api/auth/status', (request, response) => { console.log(`Inside /auth/status endpoint`); console.log(request.user); // Passport adds user to request if authenticated // Return user if authenticated, otherwise return 401 Unauthorized return request.user ? response.send(request.user) : response.sendStatus(401); });
-
Logout Endpoint:
app.post('/api/auth/logout', (request, response) => { if (!request.user) return response.sendStatus(401); request.logout((err) => { if (err) return response.sendStatus(400); response.sendStatus(200); }); });
-
Login Request: When a user submits credentials to
/api/auth
, thepassport.authenticate('local')
middleware intercepts the request. -
Strategy Execution: Passport executes our local strategy function, which validates the credentials.
-
Serialization: If authentication succeeds, the user object is passed to
serializeUser()
, which determines what data is stored in the session. -
Session Storage: Only the user ID is stored in the session for security and efficiency.
-
Subsequent Requests: For each subsequent authenticated request:
- Passport extracts the user ID from the session
- Calls
deserializeUser()
to convert the ID back into a user object - Attaches the user object to the request as
request.user
-
Authenticated Routes: Routes can check
request.user
or use therequest.isAuthenticated()
method to verify authentication. -
Logout: When a user logs out,
request.logout()
is called, which removes the user from the session.
Passport provides several helper functions for checking authentication status:
// Check if user is authenticated
const isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
res.status(401).send({ message: 'Not authenticated' });
};
// Protect routes that require authentication
app.get('/api/protected', isAuthenticated, (req, res) => {
res.send({ message: 'This is a protected route', user: req.user });
});
- Password Storage: In a production environment, never store plain-text passwords. Use bcrypt or another hashing algorithm.
- HTTPS: Always use HTTPS in production to protect session cookies from being intercepted.
- Cookie Security: Set appropriate cookie security options (httpOnly, secure, sameSite).
- Rate Limiting: Implement rate limiting on authentication endpoints to prevent brute force attacks.
- Environment Variables: Store sensitive information like session secrets in environment variables.
This implementation provides a solid foundation for authentication in an Express.js application, demonstrating both the technical setup and best practices for securing user data.