Node.js Building RESTful APIs

A REST API (Representational State Transfer Application Programming Interface) is a set of rules for building web services that allow different applications to communicate over HTTP. REST APIs are the backbone of modern web development — they allow a frontend (like a React app) to talk to a backend (Node.js server), or different backend services to exchange data.

Every action in a REST API — getting data, creating data, updating data, or deleting data — is performed using standard HTTP methods on clearly structured URLs called endpoints.

REST Principles

  • Client-Server: The client (frontend) and server (backend) are separate. They communicate through requests and responses.
  • Stateless: Each request contains all the information needed to process it. The server does not store session data between requests.
  • Uniform Interface: Resources are accessed through consistent, predictable URLs.
  • Resource-Based URLs: URLs represent resources (nouns) like /users, /products — not actions.

HTTP Methods and Their CRUD Mapping

HTTP MethodCRUD OperationExample EndpointDescription
GETReadGET /productsRetrieve all products
GETReadGET /products/:idRetrieve a single product by ID
POSTCreatePOST /productsCreate a new product
PUTUpdate (full)PUT /products/:idReplace all fields of a product
PATCHUpdate (partial)PATCH /products/:idUpdate only specific fields
DELETEDeleteDELETE /products/:idDelete a product by ID

Building a Complete RESTful API – Products Example

This example builds a full in-memory Products API with all CRUD operations. In a real application, the data would come from a database instead of an array.

Project Setup

mkdir products-api
cd products-api
npm init -y
npm install express

app.js – Full Products REST API

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

app.use(express.json()); // Parse incoming JSON bodies

// In-memory data store (replaces a real database for this example)
let products = [
  { id: 1, name: 'Laptop', price: 999, category: 'Electronics' },
  { id: 2, name: 'Desk Chair', price: 249, category: 'Furniture' },
  { id: 3, name: 'Coffee Mug', price: 15, category: 'Kitchen' }
];

let nextId = 4; // Tracks the next available ID

// ─────────────────────────────────────────────
// GET /api/products – Retrieve all products
// ─────────────────────────────────────────────
app.get('/api/products', function(req, res) {
  res.json({
    success: true,
    count: products.length,
    data: products
  });
});

// ─────────────────────────────────────────────
// GET /api/products/:id – Retrieve one product
// ─────────────────────────────────────────────
app.get('/api/products/:id', function(req, res) {
  const id = parseInt(req.params.id);
  const product = products.find(p => p.id === id);

  if (!product) {
    return res.status(404).json({ success: false, error: 'Product not found' });
  }

  res.json({ success: true, data: product });
});

// ─────────────────────────────────────────────
// POST /api/products – Create a new product
// ─────────────────────────────────────────────
app.post('/api/products', function(req, res) {
  const { name, price, category } = req.body;

  if (!name || !price || !category) {
    return res.status(400).json({
      success: false,
      error: 'name, price, and category are all required.'
    });
  }

  const newProduct = {
    id: nextId++,
    name,
    price: parseFloat(price),
    category
  };

  products.push(newProduct);

  res.status(201).json({ success: true, data: newProduct });
});

// ─────────────────────────────────────────────
// PUT /api/products/:id – Fully update a product
// ─────────────────────────────────────────────
app.put('/api/products/:id', function(req, res) {
  const id = parseInt(req.params.id);
  const index = products.findIndex(p => p.id === id);

  if (index === -1) {
    return res.status(404).json({ success: false, error: 'Product not found' });
  }

  const { name, price, category } = req.body;

  if (!name || !price || !category) {
    return res.status(400).json({
      success: false,
      error: 'All fields (name, price, category) are required for a full update.'
    });
  }

  products[index] = { id, name, price: parseFloat(price), category };

  res.json({ success: true, data: products[index] });
});

// ─────────────────────────────────────────────
// PATCH /api/products/:id – Partially update a product
// ─────────────────────────────────────────────
app.patch('/api/products/:id', function(req, res) {
  const id = parseInt(req.params.id);
  const product = products.find(p => p.id === id);

  if (!product) {
    return res.status(404).json({ success: false, error: 'Product not found' });
  }

  // Only update the fields that were sent
  if (req.body.name !== undefined)     product.name = req.body.name;
  if (req.body.price !== undefined)    product.price = parseFloat(req.body.price);
  if (req.body.category !== undefined) product.category = req.body.category;

  res.json({ success: true, data: product });
});

// ─────────────────────────────────────────────
// DELETE /api/products/:id – Delete a product
// ─────────────────────────────────────────────
app.delete('/api/products/:id', function(req, res) {
  const id = parseInt(req.params.id);
  const index = products.findIndex(p => p.id === id);

  if (index === -1) {
    return res.status(404).json({ success: false, error: 'Product not found' });
  }

  const deleted = products.splice(index, 1);

  res.json({ success: true, message: 'Product deleted', data: deleted[0] });
});

// ─────────────────────────────────────────────
// 404 – Route not found handler
// ─────────────────────────────────────────────
app.use(function(req, res) {
  res.status(404).json({ success: false, error: 'Endpoint not found' });
});

app.listen(3000, () => console.log('Products API running at http://localhost:3000'));

Testing the API with curl Commands

Get all products

curl http://localhost:3000/api/products

Get a single product

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

Create a new product

curl -X POST http://localhost:3000/api/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Keyboard","price":89,"category":"Electronics"}'

Update a product

curl -X PATCH http://localhost:3000/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{"price":1099}'

Delete a product

curl -X DELETE http://localhost:3000/api/products/3

Adding Query-Based Filtering

Filtering results using query parameters is a common REST API feature:

// GET /api/products?category=Electronics&maxPrice=500

app.get('/api/products', function(req, res) {
  let result = [...products];

  if (req.query.category) {
    result = result.filter(p =>
      p.category.toLowerCase() === req.query.category.toLowerCase()
    );
  }

  if (req.query.maxPrice) {
    result = result.filter(p => p.price <= parseFloat(req.query.maxPrice));
  }

  res.json({ success: true, count: result.length, data: result });
});

Consistent API Response Structure

A well-designed API always returns responses in a consistent format. The pattern used throughout this example is:

// Success response
{
  "success": true,
  "data": { ... }
}

// Error response
{
  "success": false,
  "error": "Description of what went wrong"
}

Consistency makes the API predictable and easy for frontend developers to consume.

Key Points

  • A RESTful API uses HTTP methods (GET, POST, PUT, PATCH, DELETE) to perform CRUD operations on resources.
  • URLs should represent resources (nouns), not actions — use /products, not /getProducts.
  • Always validate incoming data and return meaningful error messages with the correct HTTP status code.
  • Use express.json() middleware to parse JSON request bodies.
  • Return consistent response structures across all endpoints for predictability.
  • Query parameters enable flexible filtering, sorting, and pagination without changing the route.
  • In a real application, in-memory arrays are replaced by database operations using tools like Mongoose (MongoDB) or Sequelize (SQL).

Leave a Comment

Your email address will not be published. Required fields are marked *