Back to blog

Getting Started with Express.js

expressnodejsbackendjavascriptrest-api
Getting Started with Express.js

Welcome to the Express.js Learning Roadmap! This series will guide you from Express.js basics to building production-ready REST APIs and web applications with Node.js.

In this first post, you'll learn the fundamentals of Express.js, set up your development environment, create your first server, and build a simple REST API. By the end, you'll understand how Express works and be ready to tackle more advanced topics.

What You'll Learn

Learning Outcomes:
✅ Understand what Express.js is and why it's the most popular Node.js framework
✅ Set up a Node.js development environment from scratch
✅ Create your first Express server and understand how it works
✅ Handle HTTP requests (GET, POST, PUT, DELETE)
✅ Understand the middleware concept and request-response cycle
✅ Build a simple REST API with CRUD operations

What is Express.js?

Express.js (or simply "Express") is a minimal, flexible, and fast web application framework for Node.js. It's the most popular Node.js framework, with over 60 million weekly downloads on npm.

Why Express?

Express is popular because it:

  1. Minimalist and Unopinionated - Gives you freedom to structure your app as you like
  2. Fast and Lightweight - Minimal overhead, excellent performance
  3. Mature Ecosystem - Thousands of middleware packages available
  4. Easy to Learn - Simple, intuitive API that's beginner-friendly
  5. Production-Ready - Used by companies like Uber, IBM, and Accenture

Express vs Other Node.js Frameworks

FeatureExpressFastifyNestJSKoa
PhilosophyMinimalPerformanceOpinionatedMinimal
Learning CurveEasyEasyModerateEasy
PerformanceGoodExcellentGoodGood
TypeScriptOptionalBuilt-inBuilt-inOptional
Use CaseGeneral purposeHigh performanceEnterprise appsExperienced devs

When to use Express:

  • Building REST APIs or web applications
  • Need flexibility in architecture
  • Want a large ecosystem of middleware
  • Prefer simplicity and ease of learning
  • Working with a small to medium-sized team

When NOT to use Express:

  • Need maximum performance (consider Fastify)
  • Want strict TypeScript and architecture (consider NestJS)
  • Building real-time apps primarily (consider Socket.IO native)

Setting Up Your Development Environment

Prerequisites

Before we begin, make sure you have:

  • Node.js 18+ installed (check with node --version)
  • npm or yarn package manager
  • A code editor (VS Code recommended)
  • Basic JavaScript knowledge (ES6+)

Installing Node.js

If you don't have Node.js installed, download it from nodejs.org or use a version manager:

Using nvm (recommended):

# Install nvm (macOS/Linux)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
 
# Install latest LTS Node.js
nvm install --lts
nvm use --lts
 
# Verify installation
node --version  # Should show v20.x.x or higher
npm --version   # Should show v10.x.x or higher

Using Homebrew (macOS):

brew install node

Creating Your First Project

Let's create a new Express project from scratch.

Step 1: Initialize the project

# Create project directory
mkdir express-todo-api
cd express-todo-api
 
# Initialize npm project
npm init -y

This creates a package.json file with default settings.

Step 2: Install Express

npm install express

Step 3: Install development dependencies

npm install --save-dev nodemon

nodemon automatically restarts your server when files change - essential for development.

Step 4: Update package.json scripts

Edit package.json and add these scripts:

{
  "name": "express-todo-api",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

Project Structure

Here's a recommended structure for our simple project:

express-todo-api/
├── index.js          # Main application file
├── package.json      # Dependencies and scripts
├── package-lock.json # Locked dependency versions
└── node_modules/     # Installed packages

For larger projects, you'd organize code into folders like routes/, controllers/, middleware/, etc. We'll cover that in future posts.

Your First Express Server

Let's create a minimal "Hello World" server to understand the basics.

Create index.js:

// Import Express
const express = require('express');
 
// Create Express application
const app = express();
 
// Define port
const PORT = 3000;
 
// Create a route
app.get('/', (req, res) => {
  res.send('Hello World!');
});
 
// Start server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Run the server:

npm run dev

Visit http://localhost:3000 in your browser - you should see "Hello World!".

Understanding the Code

Let's break down what each part does:

const express = require('express');

Imports the Express module.

const app = express();

Creates an Express application instance. This app object has methods for routing HTTP requests, configuring middleware, rendering HTML, etc.

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

Defines a route handler for GET requests to the root path (/). The callback function receives:

  • req (request) - Contains data about the incoming request
  • res (response) - Methods to send responses back to the client
app.listen(PORT, () => { ... });

Binds the Express app to port 3000 and starts listening for connections.

Understanding Routes and HTTP Methods

Routes determine how your application responds to client requests at specific endpoints (URLs).

Route Syntax

app.METHOD(PATH, HANDLER)
  • METHOD - HTTP method (get, post, put, delete, etc.)
  • PATH - URL path
  • HANDLER - Function to execute when route is matched

HTTP Methods and CRUD

Express supports all HTTP methods, but these four map to CRUD operations:

HTTP MethodCRUD OperationPurpose
GETReadRetrieve data
POSTCreateCreate new data
PUTUpdateUpdate existing data
DELETEDeleteDelete data

Basic Route Examples

// GET request - retrieve data
app.get('/users', (req, res) => {
  res.send('Get all users');
});
 
// POST request - create data
app.post('/users', (req, res) => {
  res.send('Create a user');
});
 
// PUT request - update data
app.put('/users/:id', (req, res) => {
  res.send(`Update user ${req.params.id}`);
});
 
// DELETE request - delete data
app.delete('/users/:id', (req, res) => {
  res.send(`Delete user ${req.params.id}`);
});

Route Parameters

Use : to define route parameters:

app.get('/users/:userId', (req, res) => {
  const userId = req.params.userId;
  res.send(`User ID: ${userId}`);
});
 
// Access with: GET /users/123
// req.params.userId will be "123"

Multiple parameters:

app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
});
 
// Access with: GET /users/5/posts/42
// req.params = { userId: "5", postId: "42" }

Query Strings

Query strings pass additional data in the URL:

app.get('/search', (req, res) => {
  const { q, page, limit } = req.query;
  res.json({ query: q, page, limit });
});
 
// Access with: GET /search?q=express&page=1&limit=10
// req.query = { q: "express", page: "1", limit: "10" }

Introduction to Middleware

Middleware functions are the core concept of Express. They're functions that have access to the request object (req), response object (res), and the next middleware function in the request-response cycle.

The Request-Response Cycle

Each middleware can:

  1. Execute any code
  2. Modify request and response objects
  3. End the request-response cycle
  4. Call the next middleware in the stack

Middleware Signature

function middleware(req, res, next) {
  // Do something
  next(); // Pass control to next middleware
}

The next function passes control to the next middleware. If you don't call next(), the request will hang.

Built-in Middleware

Express has built-in middleware for common tasks:

1. express.json() - Parse JSON request bodies

// Add this before your routes
app.use(express.json());
 
app.post('/users', (req, res) => {
  // Now req.body contains parsed JSON
  const { name, email } = req.body;
  res.json({ message: 'User created', user: { name, email } });
});

2. express.urlencoded() - Parse URL-encoded bodies

app.use(express.urlencoded({ extended: true }));

3. express.static() - Serve static files

// Serve files from "public" directory
app.use(express.static('public'));
 
// Now files in public/ are accessible:
// public/style.css -> http://localhost:3000/style.css

Application-level Middleware

You can create custom middleware to run on all requests:

// Logging middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next();
});
 
// Now all requests will be logged

Execution Order Matters

Middleware executes in the order you define it:

const express = require('express');
const app = express();
 
// 1. Runs first
app.use((req, res, next) => {
  console.log('First');
  next();
});
 
// 2. Runs second (parsing JSON)
app.use(express.json());
 
// 3. Runs third
app.use((req, res, next) => {
  console.log('Third');
  next();
});
 
// 4. Route handler runs last
app.get('/', (req, res) => {
  res.send('Hello');
});

Important: Put middleware that should run on ALL requests before your routes.

Hands-on Project: Todo REST API

Let's build a simple REST API for managing todos. We'll use an in-memory array as our "database" (we'll cover real databases in later posts).

Complete Code

Replace your index.js with this:

const express = require('express');
const app = express();
const PORT = 3000;
 
// Middleware
app.use(express.json());
 
// Logging middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next();
});
 
// In-memory database
let todos = [
  { id: 1, title: 'Learn Express.js', completed: false },
  { id: 2, title: 'Build a REST API', completed: false },
];
 
// GET /api/todos - Get all todos
app.get('/api/todos', (req, res) => {
  res.json({
    success: true,
    data: todos,
  });
});
 
// GET /api/todos/:id - Get a single todo
app.get('/api/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todo = todos.find(t => t.id === id);
 
  if (!todo) {
    return res.status(404).json({
      success: false,
      message: 'Todo not found',
    });
  }
 
  res.json({
    success: true,
    data: todo,
  });
});
 
// POST /api/todos - Create a new todo
app.post('/api/todos', (req, res) => {
  const { title } = req.body;
 
  if (!title) {
    return res.status(400).json({
      success: false,
      message: 'Title is required',
    });
  }
 
  const newTodo = {
    id: todos.length + 1,
    title,
    completed: false,
  };
 
  todos.push(newTodo);
 
  res.status(201).json({
    success: true,
    data: newTodo,
  });
});
 
// PUT /api/todos/:id - Update a todo
app.put('/api/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const { title, completed } = req.body;
 
  const todoIndex = todos.findIndex(t => t.id === id);
 
  if (todoIndex === -1) {
    return res.status(404).json({
      success: false,
      message: 'Todo not found',
    });
  }
 
  // Update todo
  if (title !== undefined) todos[todoIndex].title = title;
  if (completed !== undefined) todos[todoIndex].completed = completed;
 
  res.json({
    success: true,
    data: todos[todoIndex],
  });
});
 
// DELETE /api/todos/:id - Delete a todo
app.delete('/api/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todoIndex = todos.findIndex(t => t.id === id);
 
  if (todoIndex === -1) {
    return res.status(404).json({
      success: false,
      message: 'Todo not found',
    });
  }
 
  const deletedTodo = todos.splice(todoIndex, 1)[0];
 
  res.json({
    success: true,
    data: deletedTodo,
  });
});
 
// 404 handler for unknown routes
app.use((req, res) => {
  res.status(404).json({
    success: false,
    message: 'Route not found',
  });
});
 
// Start server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Testing the API

Start the server:

npm run dev

Test with curl or a REST client:

1. Get all todos:

curl http://localhost:3000/api/todos

2. Get a single todo:

curl http://localhost:3000/api/todos/1

3. Create a new todo:

curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn middleware"}'

4. Update a todo:

curl -X PUT http://localhost:3000/api/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"completed":true}'

5. Delete a todo:

curl -X DELETE http://localhost:3000/api/todos/1

Instead of curl, use a REST client for easier testing:

  • Postman - Full-featured GUI client
  • Insomnia - Clean, simple interface
  • VS Code REST Client - Test from .http files in VS Code
  • Thunder Client - VS Code extension, lightweight

Example .http file for VS Code REST Client:

### Get all todos
GET http://localhost:3000/api/todos
 
### Get a single todo
GET http://localhost:3000/api/todos/1
 
### Create a new todo
POST http://localhost:3000/api/todos
Content-Type: application/json
 
{
  "title": "Learn middleware"
}
 
### Update a todo
PUT http://localhost:3000/api/todos/1
Content-Type: application/json
 
{
  "completed": true
}
 
### Delete a todo
DELETE http://localhost:3000/api/todos/1

Understanding the Code

1. Response Structure:

We use a consistent response format:

{
  success: true,   // or false for errors
  data: { ... },   // the actual data
  message: "..."   // optional error message
}

2. HTTP Status Codes:

  • 200 - OK (default for successful GET, PUT, DELETE)
  • 201 - Created (for successful POST)
  • 400 - Bad Request (client error, like missing required field)
  • 404 - Not Found (resource doesn't exist)
  • 500 - Internal Server Error (we'll handle this later)

3. Validation:

if (!title) {
  return res.status(400).json({
    success: false,
    message: 'Title is required',
  });
}

Always validate user input. The return keyword is important - it prevents further code execution.

4. 404 Handler:

app.use((req, res) => {
  res.status(404).json({
    success: false,
    message: 'Route not found',
  });
});

This middleware runs if no route matches. Put it at the END of your file.

Response Methods Reference

Express provides several methods to send responses:

// Send JSON
res.json({ message: 'Hello' });
 
// Send plain text
res.send('Hello World');
 
// Send status code
res.sendStatus(404);  // Sends "Not Found"
 
// Set status and send
res.status(201).json({ message: 'Created' });
 
// Redirect
res.redirect('/new-url');
 
// Download file
res.download('/path/to/file.pdf');
 
// Send file
res.sendFile('/path/to/file.html');
 
// Set headers
res.set('Content-Type', 'application/json');
res.send('{"message":"Hello"}');

Common Mistakes to Avoid

1. Not calling next() in middleware

// ❌ Wrong - request will hang
app.use((req, res, next) => {
  console.log('Hello');
  // Forgot to call next()
});
 
// ✅ Correct
app.use((req, res, next) => {
  console.log('Hello');
  next();
});

2. Sending multiple responses

// ❌ Wrong - error: "Cannot set headers after they are sent"
app.get('/test', (req, res) => {
  res.send('First');
  res.send('Second');  // Error!
});
 
// ✅ Correct - use return to prevent further execution
app.get('/test', (req, res) => {
  if (someCondition) {
    return res.send('First');
  }
  res.send('Second');
});

3. Middleware order

// ❌ Wrong - routes defined before middleware
app.get('/api/todos', (req, res) => {
  // req.body will be undefined
  const { title } = req.body;
});
 
app.use(express.json());  // Too late!
 
// ✅ Correct - middleware before routes
app.use(express.json());
 
app.get('/api/todos', (req, res) => {
  const { title } = req.body;  // Works!
});

4. Not handling errors

// ❌ Wrong - no error handling
app.get('/users/:id', (req, res) => {
  const user = database.findUser(req.params.id);  // Might throw error
  res.json(user);
});
 
// ✅ Correct - handle potential errors
app.get('/users/:id', (req, res) => {
  try {
    const user = database.findUser(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

Summary and Key Takeaways

What We Covered:
✅ Express.js is a minimal, flexible Node.js framework for building web applications and APIs
✅ Set up a Node.js project with npm init, install Express, and use nodemon for auto-reload
✅ Created routes with app.get(), app.post(), app.put(), app.delete()
✅ Understood middleware - functions that process requests before reaching route handlers
✅ Built-in middleware: express.json() for parsing JSON, express.static() for static files
✅ Built a complete CRUD REST API for managing todos with proper status codes and validation

Key Concepts:

  • Routes map URLs and HTTP methods to handler functions
  • Middleware functions execute in order and can modify req/res or call next()
  • Request object (req) contains data about the incoming request (params, query, body)
  • Response object (res) has methods to send responses (json, send, status, etc.)
  • Status codes communicate request outcome (200, 201, 400, 404, 500)

Best Practices:

  • Always call next() in middleware unless sending a response
  • Use return when sending responses to prevent multiple responses
  • Put middleware before routes
  • Validate user input
  • Use consistent response format
  • Use appropriate HTTP status codes

Practice Exercises

Try these exercises to reinforce your learning:

  1. Extend the Todo API:

    • Add filtering: GET /api/todos?completed=true
    • Add pagination: GET /api/todos?page=1&limit=10
    • Add search: GET /api/todos?search=learn
  2. Create a New API:

    • Build a simple notes API with categories
    • Each note should have: id, title, content, category, created_at
  3. Add Custom Middleware:

    • Create middleware that logs request duration
    • Create middleware that adds a timestamp to all responses
  4. Error Handling:

    • Create a centralized error handling middleware
    • Handle malformed JSON requests gracefully
  5. Response Enhancements:

    • Add pagination metadata (total, page, pages)
    • Add timestamps to all responses

What's Next?

In the next post, we'll dive deep into Express Middleware. You'll learn:

  • How middleware works under the hood
  • Creating custom middleware for authentication and logging
  • Popular third-party middleware (morgan, cors, helmet)
  • Error handling middleware patterns
  • Async error handling

This foundation will help you build more robust, production-ready applications.

Additional Resources

Official Documentation:

Learning Resources:

Tools:

Next Steps in This Series:

  • Post 2: Middleware Deep Dive
  • Post 3: Routing and Request Handling
  • Post 4: Working with MongoDB and Mongoose
  • Post 5: Working with PostgreSQL and Prisma

Keep building, and I'll see you in the next post! 🚀

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.