skip to content
Home Image

ExpressJs

/ 16 min read

Table of Contents

Nodejs Framework

Node.js frameworks provide structure, organization, and common utilities for building web applications, APIs, and more.

They help developers create applications faster by providing ready-made solutions to common development challenges.

Advantages of using a framework:

a. Productivity: Frameworks provide pre-built solutions for common tasks like routing, middleware management, and templating.

b. Standardization: They establish patterns and structures that make code more maintainable and easier to understand.

c. Community: Popular frameworks have large communities, extensive documentation, and many third-party plugins or extensions.

d. Security: Well-maintained frameworks often include built-in security features and best practices.

e. Performance: Many frameworks are optimized for performance and provide tools for caching, load balancing, and more.

Types of Node.js Frameworks

Node.js frameworks can be broadly categorized based on their design philosophy and features.

Understanding these categories helps in selecting the right framework for your project’s needs.

1. Full-Stack Frameworks

These frameworks provide solutions for both front-end and back-end development, often with integrated templating engines, ORM systems, and more.

Examples: Meteor, Sails.js, AdonisJS

Use When: Building complete web applications with both frontend and backend components

2. Minimalist/Micro Frameworks

These frameworks focus on being lightweight and provide only the essential features, letting developers add what they need.

Examples: Express.js, Koa, Fastify

Use When: Building APIs or simple web services where you want maximum control

3. REST API Frameworks

Specialized frameworks designed for building RESTful APIs with features like automatic validation, documentation, and versioning.

Examples: LoopBack, NestJS, Restify

Use When: Building robust, production-ready APIs with minimal boilerplate

4. Real-Time Frameworks

Frameworks optimized for real-time applications with built-in support for WebSockets and server-sent events.

Examples: Socket.io, Sails.js, FeathersJS

Use When: Building chat applications, live updates, or any real-time features

Express.js

Express.js (or simply Express) is the most popular Node.js web application framework, designed for building web applications and APIs.

It’s often called the de facto standard server framework for Node.js.

Q. Why Choose Express.js?

Express provides a thin layer of fundamental web application features without obscuring Node.js features.

It offers:

a. A robust routing system

b. HTTP helpers (redirection, caching, etc.)

c. Support for middleware to respond to HTTP requests

d. A templating engine for dynamic HTML rendering

e. Error handling middleware

Getting Started with Express

Before you begin, make sure you have:

. Node.js installed (v14.0.0 or later recommended)

. npm (comes with Node.js) or yarn

. A code editor (VS Code, WebStorm, etc.)

  1. Installing Express
npm install express

To install Express and save it in your package.json dependencies:

npm install express --save
  1. Basic Structure

. Importing the Express module

. Creating an Express application instance

. Defining routes

. Starting the server

const express = require('express');
const app = express();
const port = 8080;
// Define a route for GET requests to the root URL
app.get('/', (req, res) => {
res.send('Hello World from Express!');
});
// Start the server
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
  1. Basic Routing

Routing refers to how an application responds to client requests to specific endpoints (URIs) using different HTTP methods (GET, POST, PUT, DELETE, etc.).

Express provides simple methods to define routes that correspond to HTTP methods:

app.get() - Handle GET requests
app.post() - Handle POST requests
app.put() - Handle PUT requests
app.delete() - Handle DELETE requests
app.all() - Handle all HTTP methods
const express = require('express');
const app = express();
const port = 8080;
// Respond to GET request on the root route
app.get('/', (req, res) => {
res.send('GET request to the homepage');
});
// Respond to POST request on the root route
app.post('/', (req, res) => {
res.send('POST request to the homepage');
});
// Respond to GET request on the /about route
app.get('/about', (req, res) => {
res.send('About page');
});
// Catch all other routes
app.all('*', (req, res) => {
res.status(404).send('404 - Page not found');
});
// Start the server
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
  1. Route Parameters

Route parameters are named URL segments that capture values at specific positions in the URL.

They are specified in the path with a colon : prefix.

Example: /users/:userId /books/:bookId

In this example, userId and bookId are route parameters that can be accessed via req.params.

const express = require('express');
const app = express();
const port = 8080;
// Route with parameters
app.get('/users/:userId/books/:bookId', (req, res) => {
// Access parameters using req.params
res.send(`User ID: ${req.params.userId}, Book ID: ${req.params.bookId}`);
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
  1. Query Parameters

Query parameters are key-value pairs that appear after the ? in a URL.

They are automatically parsed by Express and available in req.query.

Example URL: http://example.com/search?q=express&page=2

In this URL, q=express and page=2 are query parameters that can be accessed as req.query.q and req.query.page.

const express = require('express');
const app = express();
const port = 8080;
// Route handling query parameters
app.get('/search', (req, res) => {
// Access query parameters using req.query
const { q, category } = req.query;
res.send(`Search query: ${q}, Category: ${category || 'none'}`);
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

Middleware in Express

Middleware functions are the backbone of Express applications.

They have access to:

. The request object (req)

. The response object (res)

. The next middleware function in the stack (next)

Built-in Middleware

Express includes several useful middleware functions:

express.json() - Parse JSON request bodies
express.urlencoded() - Parse URL-encoded request bodies
express.static() - Serve static files
express.Router() - Create modular route handlers
const express = require('express');
const app = express();
const port = 8080;
// Middleware to parse JSON request bodies
app.use(express.json());
// Middleware to parse URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
// Middleware to serve static files from a directory
app.use(express.static('public'));
// POST route that uses JSON middleware
app.post('/api/users', (req, res) => {
// req.body contains the parsed JSON data
console.log(req.body);
res.status(201).json({ message: 'User created', user: req.body });
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

Error Handling in Express

Error handling in Express is done through special middleware functions that have four arguments:

(err, req, res, next).

Key Points:

. Error-handling middleware must have four arguments

. It should be defined after other app.use() and route calls

. You can have multiple error-handling middleware functions

. Use next(err) to pass errors to the next error handler

const express = require('express');
const app = express();
const port = 8080;
// Route that may throw an error
app.get('/error', (req, res) => {
// Simulating an error
throw new Error('Something went wrong!');
});
// Route that uses next(error) for asynchronous code
app.get('/async-error', (req, res, next) => {
// Simulating an asynchronous operation that fails
setTimeout(() => {
try {
// Something that might fail
const result = nonExistentFunction(); // This will throw an error
res.send(result);
}
catch (error) {
next(error); // Pass errors to Express
}
}, 100);
});
// Custom error handling middleware
// Must have four parameters to be recognized as an error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

Serving Static Files

Express can serve static files like images, CSS, and JavaScript using the built-in express.static middleware.

Best Practices:

. Place static files in a dedicated directory (commonly public or static)

. Mount the static middleware before your routes

. Consider using a CDN in production for better performance

. Set appropriate cache headers for static assets

const express = require('express');
const path = require('path');
const app = express();
const port = 8080;
// Serve static files from the 'public' directory
app.use(express.static('public'));
// You can also specify a virtual path prefix
app.use('/static', express.static('public'));
// Using absolute path (recommended)
app.use('/assets', express.static(path.join(__dirname, 'public')));
app.get('/', (req, res) => {
res.send(`
<h1>Static Files Example</h1>
<img src="/images/logo.png" alt="Logo">
<link rel="stylesheet" href="/css/style.css">
<script src="/js/script.js"></script>
`);
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

Routing in Separate Files

For better organization, you can define routes in separate files using Express Router:

routes/users.js

const express = require('express');
const router = express.Router();
// Middleware specific to this router
router.use((req, res, next) => {
console.log('Users Router Time:', Date.now());
next();
});
// Define routes
router.get('/', (req, res) => {
res.send('Users home page');
});
router.get('/:id', (req, res) => {
res.send(`User profile for ID: ${req.params.id}`);
});
module.exports = router;

routes/products.js

const express = require('express');
const router = express.Router();
// Define routes
router.get('/', (req, res) => {
res.send('Products list');
});
router.get('/:id', (req, res) => {
res.send(`Product details for ID: ${req.params.id}`);
});
module.exports = router;

app.js (main file)

const express = require('express');
const usersRouter = require('./routes/users');
const productsRouter = require('./routes/products');
const app = express();
const port = 8080;
// Use the routers
app.use('/users', usersRouter);
app.use('/products', productsRouter);
app.get('/', (req, res) => {
res.send('Main application home page');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

Express.js Best Practices

Project Structure: Organize your code by feature or component

Environment Variables: Use dotenv for configuration

Error Handling: Centralize error handling

Logging: Use a logging library like morgan or winston

Security: Implement security best practices (helmet, rate limiting, etc.)

Validation: Validate input using libraries like express-validator

Testing: Write tests using jest, mocha, or similar

Production Best Practices

When deploying to production, consider these additional practices:

. Set NODE_ENV to “production”

. Use a process manager like PM2 or Forever

. Enable compression with compression middleware

. Use a reverse proxy like Nginx

. Implement proper logging and monitoring

. Set up proper error tracking

Security Best Practices

. Use Helmet to secure your Express apps by setting various HTTP headers

. Use environment variables for configuration

. Implement proper error handling

. Use HTTPS in production

. Validate user input to prevent injection attacks

. Set appropriate CORS policies

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
// Security middleware
app.use(helmet());
// CORS configuration
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Other middleware and routes
// ...

Performance Best Practices

. Use compression middleware to compress responses

. Implement proper caching strategies

. Consider using a reverse proxy (like Nginx) in front of your Express app

. Use clustering to take advantage of multi-core systems

. Optimize database queries

Middleware Concept

Middleware is a key part of Node.js web applications, particularly in Express.js.

It provides a way to add and reuse common functionality across your application’s routes and endpoints.

Key Characteristics of Middleware:

. Executes during the request-response cycle

. Can modify request and response objects

. Can end the request-response cycle

. Can call the next middleware in the stack

. Can be application-level, router-level, or route-specific

It acts as a bridge between the raw request and the final intended route handler.

How Middleware Works in the Request-Response Cycle

Middleware functions are executed in the order they are defined, creating a pipeline through which requests flow.

Each middleware function can perform operations on the request and response objects and decide whether to pass control to the next middleware or end the request-response cycle.

Lifecycle of a Request Through Middleware:

  1. Request received by the server

  2. Passed through each middleware in sequence

  3. Route handler processes the request

  4. Response flows back through middleware (in reverse order)

  5. Response sent to client

When you call next(), the next middleware in the stack is executed.

If you don’t call next(), the request-response cycle ends and no further middleware runs.

const express = require('express');
const app = express();
// First middleware
app.use((req, res, next) => {
console.log('Middleware 1: This always runs');
next();
});
// Second middleware
app.use((req, res, next) => {
console.log('Middleware 2: This also always runs');
next();
});
// Route handler
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(8080, () => {
console.log('Server running on port 8080');
});

Middleware Types

Understanding the different types of middleware helps in organizing your application’s logic effectively.

Middleware can be categorized based on its scope, purpose, and how it’s mounted in the application.

Choosing the Right Type: The type of middleware you use depends on your specific needs, such as whether the middleware should run for all requests or specific routes, and whether it needs access to the router instance.

In Node.js applications, especially with Express.js, there are several types of middleware:

  1. Application-level Middleware

Application-level middleware is bound to the Express application instance using app.use() or app.METHOD() functions.

Use Cases: Logging, authentication, request parsing, and other operations that should run for every request.

Best Practices: Define application-level middleware before defining routes to ensure they run in the correct order.

Bound to the application instance using app.use() or app.METHOD():

const express = require('express');
const app = express();
// Application-level middleware
app.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});
  1. Router-level Middleware

Router-level middleware works similarly to application-level middleware but is bound to an instance of express.Router().

Use Cases: Grouping route-specific middleware, API versioning, and organizing routes into logical groups.

Advantages: Better code organization, modular routing, and the ability to apply middleware to specific route groups.

Bound to an instance of express.Router():

const express = require('express');
const router = express.Router();
// Router-level middleware
router.use((req, res, next) => {
console.log('Router specific middleware');
next();
});
router.get('/user/:id', (req, res) => {
res.send('User profile');
});
// Add the router to the app
app.use('/api', router);
  1. Error-handling Middleware

Error-handling middleware is defined with four arguments (err, req, res, next) and is used to handle errors that occur during request processing.

Key Points:

. Must have exactly four parameters

. Should be defined after other app.use() and route calls

. Can be used to centralize error handling logic

. Can forward errors to the next error handler using next(err)

app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
  1. Third-party Middleware

The Node.js ecosystem offers numerous third-party middleware packages that extend Express functionality.

Popular Third-party Middleware:

Helmet: Secure your app by setting various HTTP headers
Morgan: HTTP request logger
CORS: Enable CORS with various options
Compression: Compress HTTP responses
Cookie-parser: Parse Cookie header and populate req.cookies

Installation Example: npm install helmet morgan cors compression cookie-parser

  1. Custom Middleware
// Authentication middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).send('Authentication required');
}
const token = authHeader.split(' ')[1];
// Verify the token (simplified)
if (token === 'secret-token') {
// Authentication successful
req.user = { id: 123, username: 'john' };
next();
} else {
res.status(403).send('Invalid token');
}
}
// Apply to specific routes
app.get('/api/protected', authenticate, (req, res) => {
res.json({ message: 'Protected data', user: req.user });
});

Best Practices

  1. Keep Middleware Focused

Each middleware should have a single responsibility, following the Single Responsibility Principle.

  1. Use Next() Properly

Always call next() unless you’re ending the response

Never call next() after sending a response

Call next() with an error parameter to trigger error handling

  1. Handle Async Code Properly

Always catch errors in async middleware and pass them to next().

  1. Don’t Overuse Middleware

Too many middleware functions can impact performance. Use them judiciously.

  1. Organize by Domain

Group related middleware in separate files based on functionality.

middleware/auth.js
exports.authenticate = (req, res, next) => {
// Authentication logic
};
exports.requireAdmin = (req, res, next) => {
// Admin verification logic
};
// In your app.js
const { authenticate, requireAdmin } = require('./middleware/auth');
app.use('/admin', authenticate, requireAdmin);
  1. Use Conditional Next()
Middleware can decide whether to continue the chain based on conditions:
// Rate limiting middleware example
function rateLimit(req, res, next) {
const ip = req.ip;
// Check if IP has made too many requests
if (tooManyRequests(ip)) {
return res.status(429).send('Too many requests');
// Note: we don't call next() here
}
// Otherwise continue
next();
}

MVC(Model, View , Controller)

MVC (Model-View-Controller) is a software design pattern commonly used for developing user interfaces that divides the related program logic into three interconnected elements.

This is done to separate internal representations of information from the ways information is presented to and accepted from the user.

  1. The Core Concept: Separation of

The primary goal of MVC is Separation of Concerns. Instead of writing one giant file that handles database connections, user input, and HTML rendering, MVC breaks the application into three specific roles:

a. Model: Data & Logic
b. View: User Interface
c. Controller: The Coordinator
  1. The Three Components

A. The Model (Data & Logic)

The Model represents the “guts” of the application. It manages the data, logic, and rules of the application.

Responsibilities:

a. Connects to the database (SQL, NoSQL, etc.).
b. Handles business logic (e.g., calculating taxes, validating a password).
c. Notifies observers if data changes.
d. Key Characteristic: The Model does not know about the View or the Controller.
It only cares about the data.

B. The View (User Interface)

The View is what the user sees and interacts with. It is the visualization of the data provided by the Model.

Responsibilities:

a. Displays data (HTML, CSS, JSON, XML).
b. Sends user actions (clicks, form submissions) to the Controller.
c. Key Characteristic: The View should remain "dumb." It should not contain complex d. logic (like calculating data); it should only display what it is told to display.

C. The Controller (The Brain/Coordinator)

The Controller acts as an interface between the Model and the View. It processes incoming requests, handles user input, and decides what to do next.

Responsibilities:

a. Receives input (HTTP requests, button clicks).
b. Asks the Model to retrieve or update data.
c. Passes that data to the View to be rendered.
d. Key Characteristic: The Controller is the traffic cop. It prevents the View and e. Model from talking directly to each other
  1. The MVC Flow (How it works step-by-step)

Imagine a user clicks a link to view their profile on a website.

Here is the lifecycle:

a. Request: The user interacts with the View (clicks "My Profile").
b. Routing: The browser sends a request to the server, which is intercepted by the Controller.
c. Processing: The Controller analyzes the request ("Oh, the user wants profile data for User ID 55").
d. Data Retrieval: The Controller tells the Model: "Get me the data for User 55."
e. Logic: The Model queries the database, gets the name and email, and returns it to the Controller.
f. Response: The Controller picks a specific View (e.g., profile.html) and feeds it the data from the Model.
g. Rendering: The View renders the HTML with the user's name populated and sends it back to the user's browser.
  1. The Restaurant Analogy

To visualize this, think of a restaurant:

The User: You (the Customer).
The View: The Menu and the Plate of Food. (This is what you see).
The Controller: The Waiter. (You give the waiter your order. The waiter takes it to the kitchen. The waiter brings the food back to you).
The Model: The Kitchen/Chef. (The Chef gathers raw ingredients [database], cooks the food [business logic], and puts it on a plate).

Why this matters:

You (User) never go into the kitchen (Model) to cook your own burger.
The Chef (Model) doesn't care who is sitting at Table 4; they just cook the burger.
The Waiter (Controller) coordinates everything but doesn't cook the food or eat it.

Benefits vs. Drawbacks

Benefits (Pros)Drawbacks (Cons)
Organization: Code is clean and easy to navigate.Complexity: Overkill for very small or simple applications (like a “Hello World” page).
Parallel Development: One developer can work on the Logic (Model) while another works on the UI (View).Learning Curve: Requires understanding of patterns and structure.
Maintainability: If you need to change the database, you only touch the Model. The View stays the same.Inefficiency: Frequent data updates can sometimes cause lag if the View/Controller connection isn’t optimized.
Testability: You can test the business logic (Model) without needing a User Interface.