Starting web development and finding yourself needing to build a backend server can feel overwhelming. If you can handle the frontend with JavaScript but wonder if server development requires learning another language, Express.js is your answer. You can build servers using JavaScript, the language you already know.

Today we’ll explore what Express.js is, why so many developers choose it, and how to get started step by step.

 

Express.js-node.js-backend-web-server-framework

 

 

1. What Makes Express.js So Popular?

Express.js is a web server framework that runs in the Node.js environment. Think of it as a pre-built toolkit for creating web servers. While you can build servers with Node.js alone, implementing everything from scratch gets complicated fast.

As of March 2025, Express 5.1.0 is the latest version. It sees approximately 29 million weekly downloads on npm and has over 66,000 stars on GitHub—the undisputed number one among Node.js web frameworks.

How Was Express Born?

In May 2010, Canadian developer TJ Holowaychuk created Express, inspired by Ruby’s Sinatra framework. What’s remarkable is that TJ created not just Express, but over 100 open source projects including Mocha, Koa, Jade (now Pug), and Commander. His code contributions exceed one million lines, leading some developers to question, “Is TJ really just one person?”

In 2014, TJ left Node.js and Express transitioned from StrongLoop → IBM → Node.js Foundation management, evolving from a personal project into a community-driven initiative.

Why Is Express So Popular?

The numbers speak for themselves. Major companies like PayPal, Uber, IBM, Fox Sports, and NASA use Express. The reasons are clear:

  • Proven Stability: Nearly 15 years of use and verification in virtually every scenario
  • Easy to Learn: Beginners can create basic servers in a day
  • Vast Ecosystem: 99% of needed functionality already exists
  • Active Community: Google searches solve most problems
  • Job Market: Express experience is overwhelmingly the most requested skill

Express is a minimalist framework that provides only what’s needed. It’s unopinionated, meaning it doesn’t force you into doing things “one way,” letting developers freely design their structure. Its lightweight core of about 500 lines adds features through middleware.

 

 

2. Express as the Core of MEAN/MERN Stack

If you’ve studied full-stack development, you’ve likely heard of MEAN or MERN stacks. What do they have in common? Express.

MEAN Stack = MongoDB + Express + Angular + Node.js
MERN Stack = MongoDB + Express + React + Node.js
MEVN Stack = MongoDB + Express + Vue + Node.js

These stacks are popular for a simple reason: you can build entire applications using only JavaScript. Frontend developers can handle backend too, making staffing efficient, and data exchange flows smoothly in JSON format.

Express plays the backend framework role in these stacks. It receives client requests, communicates with databases, and provides APIs as the middle tier. As of 2025, the MERN stack is especially popular among startups and environments requiring rapid prototyping.

 

 

3. Express vs Other Frameworks – Which Should You Choose?

While Express leads, other options exist. When should you use each?

Fastify – When Performance Really Matters

Built to be “the fastest framework.” Benchmarks show it’s 2.5-4x faster than Express—72,000 requests/second vs Express’s 18,000. Great for high-performance microservices or real-time APIs. However, the ecosystem isn’t as large as Express’s and requires learning time.

Koa – The Express Team’s Next-Gen Attempt

Created by the same team that made Express. Leverages async/await to escape callback hell with much simpler error handling. Choose Koa for more modern, cleaner code. However, the community is smaller and you’ll often need to build middleware yourself.

Hapi – Enterprise-Grade Large Services

Built by Walmart to handle Black Friday traffic. Features built-in input validation, caching, and authentication with a configuration-driven approach. Suitable for enterprise services but has a steep learning curve.

NestJS – TypeScript and Structured Architecture

A TypeScript framework inspired by Angular. Uses Express internally but provides structured architecture on top. Great for large team projects or enterprise development.

So What Should You Choose?

  • Learning or small-to-medium projects: Express
  • Extreme performance needs: Fastify
  • Clean modern code preference: Koa
  • Large-scale enterprise: Hapi or NestJS

But Express is usually the answer. Vast resources, easy learning, proven stability, and most importantly, the most demanded skill in job postings.

 

 

4. Installation to First Server – Hands-On Practice

Using Express requires Node.js installed first. Express 5.x requires Node.js 18 or higher.

Check Node.js Installation

Open your terminal and enter:

node --version
npm --version

If versions display, you’re already set. If not, download the LTS version from nodejs.org.

Create Your Project

# 1. Create project folder
mkdir my-express-app
cd my-express-app

# 2. Create package.json
npm init -y

# 3. Install Express
npm install express

# 4. Install nodemon for development convenience (optional)
npm install --save-dev nodemon

What’s nodemon?

Tired of restarting your server every time you modify code? nodemon automatically restarts the server when files change. Really convenient for development.

Add scripts to package.json

Open your package.json file and add this section:

{
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  }
}

Now you can run development servers with npm run dev and production servers with npm start.

Write Your First Server Code

Create app.js and write this code:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Run the Server

npm run dev

Visit http://localhost:3000 in your browser to see “Hello World!” Congratulations! You just created your first Express server.

Understanding Each Line

  • const express = require('express'): Imports the Express module
  • const app = express(): Creates an Express app instance
  • app.get('/', ...): Defines a function to execute when GET requests arrive at root path (/)
  • req: Request object containing client request information
  • res: Response object used to send responses to clients
  • app.listen(3000, ...): Starts the server on port 3000

 

 

5. req and res Objects – Handling Requests and Responses

You need to understand the two most frequently used objects in Express.

req (Request Object) Key Methods

app.get('/user/:id', (req, res) => {
  // URL parameters
  const userId = req.params.id;
  
  // Query parameters (?name=john&age=25)
  const name = req.query.name;
  const age = req.query.age;
  
  // POST data (requires express.json())
  const data = req.body;
  
  // Header information
  const userAgent = req.get('User-Agent');
  
  // Cookies (requires cookie-parser)
  const sessionId = req.cookies.sessionId;
  
  // Request method and path
  console.log(req.method); // GET, POST, PUT, DELETE, etc.
  console.log(req.path);   // /user/123
  console.log(req.url);    // /user/123?name=john
});

res (Response Object) Key Methods

app.get('/api/demo', (req, res) => {
  // 1. Text response
  res.send('Hello World');
  
  // 2. JSON response (most used in APIs)
  res.json({ name: 'John Doe', age: 25 });
  
  // 3. Response with status code
  res.status(404).json({ error: 'Not found' });
  
  // 4. Redirect
  res.redirect('/login');
  
  // 5. File download
  res.download('/files/report.pdf');
  
  // 6. Send file
  res.sendFile('/path/to/file.html');
  
  // 7. Send status code only
  res.sendStatus(204); // No Content
});

Commonly Used Status Codes

  • 200: Success
  • 201: Created successfully (used in POST)
  • 204: Success with no content (used in DELETE)
  • 400: Bad request
  • 401: Authentication required
  • 403: Forbidden
  • 404: Not found
  • 500: Server error

 

 

6. Middleware – Express’s Real Power

Middleware is Express’s core. Express apps are essentially a series of middleware function calls.

What Is Middleware?

Functions executed in the middle process from when requests arrive at the server until responses go out. Think of airport security checkpoints—baggage inspection, ID verification, metal detectors, one after another. Middleware works the same way.

Real-World Uses

  • Request logging (who, when, which page)
  • User authentication (checking login status)
  • Data parsing (converting JSON or form data to readable format)
  • Error handling (consistent problem management)
  • CORS configuration (allowing cross-domain requests)
  • Security header addition

Writing Basic Middleware

const express = require('express');
const app = express();

// 1. Logging middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next(); // Must call to move to next
});

// 2. JSON parsing middleware (essential!)
app.use(express.json());

// 3. URL-encoded data parsing (form data)
app.use(express.urlencoded({ extended: true }));

// 4. Serve static files (HTML, CSS, images, etc.)
app.use(express.static('public'));

app.get('/', (req, res) => {
  res.send('Home Page');
});

app.listen(3000);

Difference Between express.json() and express.urlencoded()

// express.json() - For receiving JSON data in APIs
// Content-Type: application/json
// { "name": "John Doe", "age": 25 }
app.use(express.json());

// express.urlencoded() - For receiving data from HTML forms
// Content-Type: application/x-www-form-urlencoded
// name=John+Doe&age=25
app.use(express.urlencoded({ extended: true }));

Serving Static Files

Used for serving HTML, CSS, images, JavaScript files:

const express = require('express');
const path = require('path');
const app = express();

// Serve files from public folder
app.use(express.static('public'));

// Set virtual path
app.use('/static', express.static('public'));

// Use absolute path (recommended)
app.use(express.static(path.join(__dirname, 'public')));

Folder structure:

my-express-app/
  ├── public/
  │   ├── css/
  │   │   └── style.css
  │   ├── js/
  │   │   └── app.js
  │   └── images/
  │       └── logo.png
  └── app.js

Now you can access files at http://localhost:3000/css/style.css.

Authentication Middleware Example

// Login check middleware
const checkAuth = (req, res, next) => {
  if (req.headers.authorization) {
    next(); // Authentication passed
  } else {
    res.status(401).json({ error: 'Login required' });
  }
};

// Apply to specific routes
app.get('/api/profile', checkAuth, (req, res) => {
  res.json({ name: 'John Doe', email: 'john@example.com' });
});

Important Notes

  • Middleware executes in order
  • Must call next() to move to next middleware
  • Without calling next(), requests get stuck
  • Error handling middleware must be positioned last

 

 

7. Routing and REST API – Practical Examples

Routing means sending different responses based on URL and HTTP method (GET, POST, PUT, DELETE).

Basic Routing

const express = require('express');
const app = express();

app.use(express.json());

// GET: Retrieve data
app.get('/users', (req, res) => {
  res.send('User list');
});

// POST: Create new data
app.post('/users', (req, res) => {
  res.send('Create new user');
});

// PUT: Update data
app.put('/users/:id', (req, res) => {
  res.send(`Update user ${req.params.id}`);
});

// DELETE: Delete data
app.delete('/users/:id', (req, res) => {
  res.send(`Delete user ${req.params.id}`);
});

app.listen(3000);

Dynamic Routing – Receiving Values from URLs

// Using URL parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.send(`User ${userId}'s post ${postId}`);
});

// Using query parameters
app.get('/search', (req, res) => {
  const { keyword, page = 1 } = req.query;
  res.send(`Keyword: ${keyword}, Page: ${page}`);
});
// Access: /search?keyword=express&page=2

Complete REST API Example

const express = require('express');
const app = express();
app.use(express.json());

// Temporary database
let users = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

// Retrieve all users
app.get('/api/users', (req, res) => {
  res.json(users);
});

// Retrieve specific user
app.get('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// Add new user
app.post('/api/users', (req, res) => {
  const newUser = {
    id: users.length + 1,
    name: req.body.name,
    email: req.body.email
  };
  users.push(newUser);
  res.status(201).json(newUser);
});

// Update user information
app.put('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  user.name = req.body.name || user.name;
  user.email = req.body.email || user.email;
  res.json(user);
});

// Delete user
app.delete('/api/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  users.splice(index, 1);
  res.status(204).send();
});

app.listen(3000, () => {
  console.log('API server running on port 3000');
});

Testing Your API

Once you’ve built an API, you need to test it. Browsers can only test GET, so use specialized tools:

  1. Postman (postman.com): Most famous with extensive features
  2. Thunder Client: VS Code extension, lightweight and convenient
  3. curl: Use from terminal
# curl usage examples
# GET request
curl http://localhost:3000/api/users

# POST request
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Bob Wilson","email":"bob@example.com"}'

# PUT request
curl -X PUT http://localhost:3000/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe (Updated)","email":"john_new@example.com"}'

# DELETE request
curl -X DELETE http://localhost:3000/api/users/1

 

 

8. Code Organization – Router and Project Structure

As projects grow, writing all routes in one file becomes difficult. Using Router lets you separate files by functionality.

Recommended Project Structure

my-express-app/
  ├── src/
  │   ├── routes/          # Route definitions
  │   │   ├── users.js
  │   │   └── posts.js
  │   ├── controllers/     # Business logic
  │   │   ├── userController.js
  │   │   └── postController.js
  │   ├── middleware/      # Custom middleware
  │   │   ├── auth.js
  │   │   └── errorHandler.js
  │   ├── models/          # Data models
  │   │   └── User.js
  │   └── utils/           # Utility functions
  │       └── validator.js
  ├── public/              # Static files
  │   ├── css/
  │   ├── js/
  │   └── images/
  ├── .env                 # Environment variables
  ├── .gitignore
  ├── app.js               # Main file
  └── package.json

routes/users.js

const express = require('express');
const router = express.Router();

// GET /api/users
router.get('/', (req, res) => {
  res.json({ message: 'User list' });
});

// GET /api/users/:id
router.get('/:id', (req, res) => {
  res.json({ message: `User ${req.params.id} details` });
});

// POST /api/users
router.post('/', (req, res) => {
  res.status(201).json({ message: 'New user created' });
});

module.exports = router;

app.js

const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');

const app = express();

// Middleware
app.use(express.json());
app.use(express.static('public'));

// Connect routers
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);

// 404 handling
app.use((req, res) => {
  res.status(404).json({ error: 'Page not found' });
});

// Error handling
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Server error occurred' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

 

 

9. Error Handling and Debugging

Errors inevitably occur in production. Without proper handling, servers crash.

Error Handling Middleware

const express = require('express');
const app = express();

// Regular routes
app.get('/', (req, res) => {
  res.send('Normal operation');
});

app.get('/error', (req, res) => {
  throw new Error('Intentional error!');
});

// 404 error handling (when route doesn't exist)
app.use((req, res, next) => {
  res.status(404).json({ error: 'Page not found' });
});

// Error handling middleware (must be last!)
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  // Detailed error message in development, hidden in production
  const errorMessage = process.env.NODE_ENV === 'development' 
    ? err.message 
    : 'Server error occurred';
  
  res.status(err.status || 500).json({ error: errorMessage });
});

app.listen(3000);

Enhanced Error Handling in Express 5

Express 5 automatically catches async/await errors:

// Express 5 handles errors automatically without try-catch
app.get('/user/:id', async (req, res) => {
  const user = await db.findUser(req.params.id); // Errors go to error handler automatically
  res.json(user);
});

Debugging Tips

  1. Using console.log
app.use((req, res, next) => {
  console.log('=== Request Info ===');
  console.log('Method:', req.method);
  console.log('URL:', req.url);
  console.log('Body:', req.body);
  console.log('===================');
  next();
});
  1. Using VS Code Debugger

Create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Express",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/app.js"
    }
  ]
}

Now press F5 to run in debug mode and set breakpoints.

 

 

10. Production Ready – Practical Tips

Environment Variables Management

npm install dotenv

.env file:

NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
API_KEY=your-api-key

Add .env to .gitignore (important!):

node_modules/
.env

Use in app.js:

require('dotenv').config();

const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;

Security Enhancement

npm install helmet cors express-rate-limit
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');

// Security header configuration
app.use(helmet());

// CORS configuration
const corsOptions = {
  origin: ['http://localhost:3000', 'https://yourdomain.com'], // Allowed domains
  credentials: true, // Allow cookie transmission
  optionsSuccessStatus: 200
};
app.use(cors(corsOptions));

// API rate limiting (DDoS protection)
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Maximum 100 requests per IP
  message: 'Too many requests. Please try again later.'
});
app.use('/api/', limiter);

Logging Configuration

npm install morgan
const morgan = require('morgan');

// Development environment: simple logs
if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
}

// Production environment: detailed logs
if (process.env.NODE_ENV === 'production') {
  app.use(morgan('combined'));
}

Input Validation

npm install joi
const Joi = require('joi');

// Validation when creating users
app.post('/api/users', (req, res) => {
  const schema = Joi.object({
    name: Joi.string().min(2).max(30).required(),
    email: Joi.string().email().required(),
    age: Joi.number().integer().min(0).max(120)
  });

  const { error, value } = schema.validate(req.body);
  
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  
  // Use validated data
  res.json({ message: 'Created successfully', user: value });
});

 

 

11. Major Changes in Express 5

Express 5.0 was released in October 2024 after 10 years:

  • Node.js 18+ Required: Latest JavaScript features available
  • Automatic Error Handling: Automatically catches async/await errors
  • Stricter Path Matching: /foo* must change to /foo(.*)
  • Performance Improvements: Faster with legacy code removal
  • New Features: Uint8Array support in res.send(), etc.

Most Express 4 code works without major modifications, but see the migration guide for details.

 

 

12. Next Steps – What to Learn

Once you’ve mastered Express basics, you’re ready for the next level:

Database Connection

  • MongoDB + Mongoose: NoSQL, quick to start
  • PostgreSQL + Prisma: Relational DB, strong for complex queries
  • MySQL + Sequelize: Most widely used

Authentication Implementation

  • JWT: Token-based authentication, suitable for APIs
  • Passport.js: Social login (Google, Facebook, etc.)
  • bcrypt: Password encryption

Frontend Connection

  • React: Most popular choice
  • Vue: Easy to learn
  • Angular: Suitable for large projects

Testing

  • Jest: Unit testing
  • Supertest: API endpoint testing
  • Mocha + Chai: Traditional testing tools

Deployment

  • Docker: Containerization
  • PM2: Process management (zero-downtime restart, load balancing)
  • Nginx: Reverse proxy
  • Cloud: AWS, Heroku, Render, Vercel

 

 

Express.js feels unfamiliar at first but becomes a truly convenient tool once you’re comfortable with it. Being able to develop from frontend to backend using only JavaScript is a major advantage—precisely why the MERN stack is popular.

Recommended Learning Resources

Express is a proven framework trusted by developers for nearly 15 years. It still holds first place amid rapidly changing trends not simply because it came first, but because it’s a practical, stable, and highly productive tool. 🙂

 

Leave a Reply