Blog Post

How to Build a REST API with Node.js and Express: Complete Tutorial

The Backend Foundation

Building a REST API is a mandatory skill for any backend or full-stack developer. It acts as the brain and router of your application, validating incoming data from frontends (like React or iOS apps) and reading/writing to your database.

In this comprehensive tutorial, we will build a complete API from scratch using Node.js and the Express framework. We will implement all four essential CRUD operations (Create, Read, Update, Delete) and ensure we use the correct HTTP status codes.

Step 1: Scaffolding the Server

First, open your terminal, create a new folder, and install Express. We'll also install nodemon so our server automatically restarts when we edit code.

mkdir node-api && cd node-api
npm init -y
npm install express
npm install -D nodemon

Create a file named index.js and set up the foundation of the Express server. Note the critically important express.json() middleware—without it, your API cannot read JSON bodies sent from clients!

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

// Middleware to parse incoming JSON payloads
app.use(express.json());

// In-memory array acting as our temporary database
let books = [
  { id: 1, title: 'Dune', author: 'Frank Herbert' },
  { id: 2, title: '1984', author: 'George Orwell' }
];

app.listen(PORT, () => console.log(`Server live on http://localhost:${PORT}`));

Step 2: Implementing the CRUD Routes

READ: Getting Data (GET)

To fetch data, we use the `app.get()` method. We will create one route to fetch all books, and one route to fetch a single book by its URL parameter ID.

// GET ALL
app.get('/api/books', (req, res) => {
  res.status(200).json(books);
});

// GET ONE
app.get('/api/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const book = books.find(b => b.id === bookId);

  if (!book) return res.status(404).json({ error: 'Book not found' });
  res.status(200).json(book);
});

CREATE: Adding Data (POST)

To accept incoming data, we read req.body. Notice we use status code 201 Created instead of the standard 200.

// CREATE
app.post('/api/books', (req, res) => {
  const { title, author } = req.body;
  
  if (!title || !author) {
    return res.status(400).json({ error: 'Title and author are required' });
  }

  const newBook = { id: books.length + 1, title, author };
  books.push(newBook);
  
  res.status(201).json(newBook);
});

UPDATE & DELETE (PUT & DELETE)

To modify an existing resource, we locate it by ID, update its properties, and return the newly modified object.

// UPDATE
app.put('/api/books/:id', (req, res) => {
  const book = books.find(b => b.id === parseInt(req.params.id));
  if (!book) return res.status(404).json({ error: 'Book not found' });

  book.title = req.body.title || book.title;
  book.author = req.body.author || book.author;
  res.status(200).json(book);
});

// DELETE
app.delete('/api/books/:id', (req, res) => {
  const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
  if (bookIndex === -1) return res.status(404).json({ error: 'Book not found' });

  books.splice(bookIndex, 1);
  res.status(204).send(); // 204 No Content is best practice for Deletes
});

Conclusion & Next Steps

You now have a fully functional API architecture capable of processing incoming data, validating requirements, assigning the correct HTTP status codes (200, 201, 400, 404, 204), and manipulating state. The natural next step in your backend journey is replacing the hardcoded let books = [] array with a persistent Postgres or MongoDB database connection!