Build a REST API with Node.js and Express from Scratch: Complete Tutorial

Why Node.js and Express for REST APIs?

Node.js and Express remain one of the most popular combinations for building REST APIs in 2026. Node's non-blocking I/O makes it excellent for handling concurrent requests, and Express provides a minimal, unopinionated framework that gives you complete control over your API's architecture. Whether you're building a side project or a production service, this tutorial will walk you through everything you need.

Prerequisites

  • Node.js 18+ installed
  • Basic JavaScript knowledge
  • A terminal and code editor (VS Code recommended)

Step 1: Project Setup

Create a new directory and initialize your project:

mkdir my-api && cd my-api
npm init -y
npm install express
npm install --save-dev nodemon

Add a start script to package.json:

"scripts": {
  "start": "node src/index.js",
  "dev": "nodemon src/index.js"
}

Step 2: Create Your First Express Server

Create src/index.js:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Run npm run dev and visit http://localhost:3000/health to confirm it's working.

Step 3: Organize with Routes

Create src/routes/users.js:

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

// In-memory data store (replace with database later)
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

// GET /users - list all users
router.get('/', (req, res) => {
  res.json(users);
});

// GET /users/:id - get single user
router.get('/: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);
});

// POST /users - create user
router.post('/', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }
  const user = { id: users.length + 1, name, email };
  users.push(user);
  res.status(201).json(user);
});

// PUT /users/:id - update user
router.put('/: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[index] = { ...users[index], ...req.body };
  res.json(users[index]);
});

// DELETE /users/:id - delete user
router.delete('/: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();
});

module.exports = router;

Register the router in src/index.js:

const usersRouter = require('./routes/users');
app.use('/api/users', usersRouter);

Step 4: Add Error Handling Middleware

Add centralized error handling at the bottom of src/index.js:

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

// Global error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal server error'
  });
});

Step 5: Add Request Validation

Install express-validator for robust input validation:

npm install express-validator

Use it in your routes:

const { body, validationResult } = require('express-validator');

router.post('/',
  body('email').isEmail(),
  body('name').notEmpty().trim(),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // ... create user
  }
);

Step 6: Connect to a Database

Replace the in-memory array with a real database. For a quick start with MongoDB:

npm install mongoose

Create src/models/User.js:

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true }
}, { timestamps: true });

module.exports = mongoose.model('User', userSchema);

Best Practices Summary

  • Always validate and sanitize input
  • Use environment variables for configuration (dotenv)
  • Implement proper error handling at every layer
  • Use HTTP status codes correctly (200, 201, 400, 401, 404, 500)
  • Version your API (e.g., /api/v1/users)
  • Add rate limiting to prevent abuse
  • Use HTTPS in production

Conclusion

You now have a solid foundation for building REST APIs with Node.js and Express. Start simple, add features incrementally, and always prioritize clean error handling and input validation. From here, you can add JWT authentication, connect to your preferred database, and deploy to services like Railway, Render, or AWS.