REST APIs stand for Representational State Transfer, and they are the backbone of modern software development, enabling communication between systems.
For example, when you’re reading this blog on the internet, your browser (or app) sends a request to the Bruno blog website. The website then responds with a web page listing the blogs. Behind the scenes, your client (mobile, desktop, etc.) is calling an API to access this website, and the API serves the response back to you
Imagine you're at a restaurant. You don't go into the kitchen to cook your meal; instead, you tell the waiter what you want, and they relay your order to the kitchen. The kitchen prepares it, and the waiter brings it back to you.
In the digital realm, an API (Application Programming Interface) acts like that waiter. It's a set of rules and protocols that allows different software applications to communicate with each other.
While there are various types of APIs, one of the most popular and widely adopted styles is REST (Representational State Transfer). A REST API adheres to a set of architectural constraints that make it simple, scalable, and stateless.
To build a REST API, we will use Node.js and Express.js as the backend framework.
Your API will:
Here’s how you might set up a simple CRUD (Create, Read, Update, Delete) API for managing books using Express.js. We'll break down the code into sections for clarity and then provide the full runnable example.
This section handles the initial setup of our Express.js application, including module imports, middleware, and our temporary in-memory data store.
// Import necessary modules
const express = require('express');
const { v4: uuidv4 } = require('uuid'); // For generating unique IDs
// Initialize the Express application
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON request bodies
// This allows Express to read JSON data sent in POST, PUT, PATCH requests
app.use(express.json());
// In-memory "database" for our books
// In a real application, this would be a persistent database (e.g., PostgreSQL, MongoDB)
let books = [
{
id: uuidv4(),
title: 'The Lord of the Rings',
author: 'J.R.R. Tolkien',
publicationYear: 1954,
},
{
id: uuidv4(),
title: 'Pride and Prejudice',
author: 'Jane Austen',
publicationYear: 1813,
},
{
id: uuidv4(),
title: '1984',
author: 'George Orwell',
publicationYear: 1949,
},
];
These endpoints handle requests to retrieve information about books.
// --- GET API Endpoints (Read Operations) ---
// GET /api/books - Get all books
// Returns a list of all books in our collection.
app.get('/api/books', (req, res) => {
res.status(200).json(books);
});
// GET /api/books/:id - Get a single book by ID
// Retrieves a specific book using its unique ID from the URL parameters.
app.get('/api/books/:id', (req, res) => {
const { id } = req.params; // Extract the ID from the URL
const book = books.find((b) => b.id === id); // Find the book in our 'database'
if (!book) {
// If no book is found, return a 404 Not Found error
return res.status(404).json({ message: `Book with ID '${id}' not found.` });
}
// If found, return the book with a 200 OK status
res.status(200).json(book);
});
This endpoint handles requests to add a new book to our collection.
// --- POST API Endpoint (Create Operation) ---
// POST /api/books - Create a new book
// Adds a new book to the collection based on data provided in the request body.
app.post('/api/books', (req, res) => {
const { title, author, publicationYear } = req.body; // Extract data from the request body
// Basic validation: ensure title and author are provided
if (!title || !author) {
return res
.status(400) // 400 Bad Request if essential data is missing
.json({ message: 'Title and author are required fields.' });
}
// Create a new book object with a unique ID
const newBook = {
id: uuidv4(), // Generate a unique ID for the new book
title,
author,
publicationYear: publicationYear || null, // Allow publicationYear to be optional
};
books.push(newBook); // Add the new book to our in-memory array
res.status(201).json(newBook); // 201 Created: Indicates successful resource creation
});
This endpoint handles requests to completely replace an existing book's data.
// --- PUT API Endpoint (Full Update Operation) ---
// PUT /api/books/:id - Update a book (full replacement)
// Replaces an entire book's data identified by its ID with the new data from the request body.
app.put('/api/books/:id', (req, res) => {
const { id } = req.params; // Get the ID from the URL
const { title, author, publicationYear } = req.body; // Get new data from the request body
// Basic validation for the replacement data
if (!title || !author) {
return res
.status(400)
.json({ message: 'Title and author are required fields for PUT.' });
}
// Find the index of the book to be updated
const bookIndex = books.findIndex((b) => b.id === id);
if (bookIndex === -1) {
// If book not found, return 404 Not Found
return res
.status(404)
.json({ message: `Book with ID '${id}' not found for update.` });
}
// Create an updated book object, keeping the original ID
const updatedBook = {
id, // Keep the existing ID
title,
author,
publicationYear: publicationYear || null,
};
books[bookIndex] = updatedBook; // Replace the old book with the updated one
res.status(200).json(updatedBook); // 200 OK: Indicates successful update
});
This endpoint handles requests to partially modify an existing book's data.
// --- PATCH API Endpoint (Partial Update Operation) ---
// PATCH /api/books/:id - Partially update a book
// Modifies only the specified fields of an existing book.
app.patch('/api/books/:id', (req, res) => {
const { id } = req.params; // Get the ID from the URL
const updates = req.body; // Get partial update data from the request body
// Find the index of the book to be updated
const bookIndex = books.findIndex((b) => b.id === id);
if (bookIndex === -1) {
// If book not found, return 404 Not Found
return res
.status(404)
.json({ message: `Book with ID '${id}' not found for partial update.` });
}
// Apply updates: use spread operator to merge existing book data with new updates
books[bookIndex] = { ...books[bookIndex], ...updates };
res.status(200).json(books[bookIndex]); // 200 OK: Return the updated book
});
This endpoint handles requests to remove a book from the collection.
// --- DELETE API Endpoint (Delete Operation) ---
// DELETE /api/books/:id - Delete a book
// Removes a book from the collection identified by its ID.
app.delete('/api/books/:id', (req, res) => {
const { id } = req.params; // Get the ID from the URL
const initialLength = books.length; // Store initial length to check if a book was actually removed
books = books.filter((b) => b.id !== id); // Filter out the book to be deleted
if (books.length === initialLength) {
// If no book was removed (length didn't change), it means the ID wasn't found
return res
.status(404)
.json({ message: `Book with ID '${id}' not found for deletion.` });
}
// 204 No Content: Indicates successful deletion with no response body
res.status(204).send();
});
Here's the full code for the Express.js Books REST API, which you can save as app.js
and run.
// Import necessary modules
const express = require('express');
const { v4: uuidv4 } = require('uuid'); // For generating unique IDs
// Initialize the Express application
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON request bodies
// This allows Express to read JSON data sent in POST, PUT, PATCH requests
app.use(express.json());
// In-memory "database" for our books
// In a real application, this would be a persistent database (e.g., PostgreSQL, MongoDB)
let books = [
{
id: uuidv4(),
title: 'The Lord of the Rings',
author: 'J.R.R. Tolkien',
publicationYear: 1954,
},
{
id: uuidv4(),
title: 'Pride and Prejudice',
author: 'Jane Austen',
publicationYear: 1813,
},
{
id: uuidv4(),
title: '1984',
author: 'George Orwell',
publicationYear: 1949,
},
];
// --- API Endpoints ---
// GET /api/books - Get all books
// Returns a list of all books in our collection.
app.get('/api/books', (req, res) => {
res.status(200).json(books);
});
// GET /api/books/:id - Get a single book by ID
// Retrieves a specific book using its unique ID from the URL parameters.
app.get('/api/books/:id', (req, res) => {
const { id } = req.params; // Extract the ID from the URL
const book = books.find((b) => b.id === id); // Find the book in our 'database'
if (!book) {
// If no book is found, return a 404 Not Found error
return res.status(404).json({ message: `Book with ID '${id}' not found.` });
}
// If found, return the book with a 200 OK status
res.status(200).json(book);
});
// POST /api/books - Create a new book
// Adds a new book to the collection based on data provided in the request body.
app.post('/api/books', (req, res) => {
const { title, author, publicationYear } = req.body; // Extract data from the request body
// Basic validation: ensure title and author are provided
if (!title || !author) {
return res
.status(400) // 400 Bad Request if essential data is missing
.json({ message: 'Title and author are required fields.' });
}
// Create a new book object with a unique ID
const newBook = {
id: uuidv4(), // Generate a unique ID for the new book
title,
author,
publicationYear: publicationYear || null, // Allow publicationYear to be optional
};
books.push(newBook); // Add the new book to our in-memory array
res.status(201).json(newBook); // 201 Created: Indicates successful resource creation
});
// PUT /api/books/:id - Update a book (full replacement)
// Replaces an entire book's data identified by its ID with the new data from the request body.
app.put('/api/books/:id', (req, res) => {
const { id } = req.params; // Get the ID from the URL
const { title, author, publicationYear } = req.body; // Get new data from the request body
// Basic validation for the replacement data
if (!title || !author) {
return res
.status(400)
.json({ message: 'Title and author are required fields for PUT.' });
}
const bookIndex = books.findIndex((b) => b.id === id); // Find the index of the book to be updated
if (bookIndex === -1) {
// If book not found, return 404 Not Found
return res
.status(404)
.json({ message: `Book with ID '${id}' not found for update.` });
}
// Create an updated book object, keeping the original ID
const updatedBook = {
id, // Keep the existing ID
title,
author,
publicationYear: publicationYear || null,
};
books[bookIndex] = updatedBook; // Replace the old book with the updated one
res.status(200).json(updatedBook); // 200 OK: Indicates successful update
});
// PATCH /api/books/:id - Partially update a book
// Modifies only the specified fields of an existing book.
app.patch('/api/books/:id', (req, res) => {
const { id } = req.params; // Get the ID from the URL
const updates = req.body; // Get partial update data from the request body
const bookIndex = books.findIndex((b) => b.id === id); // Find the index of the book to be updated
if (bookIndex === -1) {
// If book not found, return 404 Not Found
return res
.status(404)
.json({ message: `Book with ID '${id}' not found for partial update.` });
}
// Apply updates: use spread operator to merge existing book data with new updates
books[bookIndex] = { ...books[bookIndex], ...updates };
res.status(200).json(books[bookIndex]); // 200 OK: Return the updated book
});
// DELETE /api/books/:id - Delete a book
// Removes a book from the collection identified by its ID.
app.delete('/api/books/:id', (req, res) => {
const { id } = req.params; // Get the ID from the URL
const initialLength = books.length; // Store initial length to check if a book was actually removed
books = books.filter((b) => b.id !== id); // Filter out the book to be deleted
if (books.length === initialLength) {
// If no book was removed (length didn't change), it means the ID wasn't found
return res
.status(404)
.json({ message: `Book with ID '${id}' not found for deletion.` });
}
// 204 No Content: Indicates successful deletion with no response body
res.status(204).send();
});
// --- Server Activation ---
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
To run this example, save it as `server.js` and install dependencies:
npm install express uuid
node app.js
Once the server is running, you can test it in your browser or with an API client:
http://localhost:3000/api/books
Example Response (GET /api/books):
[{
"id": "some-uuid-1",
"title": "The Lord of the Rings",
"author": "J.R.R. Tolkien",
"publicationYear": 1954
},
{
"id": "some-uuid-2",
"title": "Pride and Prejudice",
"author": "Jane Austen",
"publicationYear": 1813
}
]
Now, let's talk about the tools that make this process much smoother!
Before you even think about deploying, you need to rigorously test your API. This is where an API client becomes your best friend. Bruno is an open-source and local-first API testing client.
Why, Bruno?
Bruno simplifies testing your REST APIs. You can easily create requests for each HTTP method (GET, POST, PUT, PATCH, DELETE) and define headers, body content, and even pre-request or post-response scripts for more complex scenarios.
This is a basic example of how you can use Bruno and write your test cases against your endpoint. Imagine you have hundreds of APIs, and you want to test, check performance, add validations, etc. Then you need to have a secure testing environment like Bruno.
We’ve published the full working collection on GitHub. You can either clone this or simply click the Fetch in Bruno button below!
To deploy your Express.js API to a cloud service like Render, follow these general steps:
Your project is deployed now, and feel free to check out the GitHub repo with all the source code.
REST APIs are the cornerstone of distributed systems, offering a flexible, scalable, and widely understood approach to building web services. By leveraging frameworks like Express.js for development and API tools like Bruno for testing, you can efficiently build, test, and maintain robust APIs.
Bruno’s local-first, Git-friendly design makes it an ideal companion for backend developers focused on reliable and collaborative API workflows. Download Bruno today and streamline your REST API development!
Join our Discord server because your APIs deserve better collaboration.