
How to Build a REST API with Node.js and Express: A Complete Step-by-Step Tutorial
If you’ve spent any time in web development, you’ve almost certainly heard the term REST API thrown around. Maybe you’re building a mobile app that needs a backend, or you’re working on a project where a frontend and backend need to communicate, or you simply want to understand how modern web services actually work under the hood. Whatever brought you here, this tutorial will walk you through building a fully functional REST API from scratch using Node.js and Express, one of the most popular and practical skill sets in backend development today.
By the end of this guide, you’ll have a working API, a solid understanding of how RESTful architecture works, and the confidence to extend what you’ve built into something much larger.
What Is a REST API and Why Does It Matter?
Before writing a single line of code, it’s worth taking a moment to understand what we’re actually building. REST stands for Representational State Transfer. It’s an architectural style — not a protocol or a standard — that defines a set of constraints for how web services should communicate. A REST API is essentially a set of rules and endpoints that allow different applications to talk to each other over HTTP.
Think of it like a restaurant. You’re the client, the kitchen is the server, and the menu is the API. You don’t walk into the kitchen and cook your own food — you place an order through a structured interface (the menu), the kitchen handles the request, and you receive a response (your meal). The menu defines exactly what you can ask for and what you’ll get back.
REST APIs use standard HTTP methods to perform operations on data. The four most common are GET (retrieve data), POST (create new data), PUT or PATCH (update existing data), and DELETE (remove data). These map closely to the CRUD operations — Create, Read, Update, Delete — that form the backbone of most data-driven applications.
What You’ll Need Before Getting Started
This tutorial assumes you have a basic familiarity with JavaScript, but you don’t need to be an expert. You should have Node.js installed on your machine. If you haven’t done that yet, head over to nodejs.org and download the LTS version, which is the most stable release recommended for most users.
Once Node.js is installed, you’ll also have access to npm, the Node Package Manager, which we’ll use to install Express and a few other helpful tools. You can verify your installation by opening a terminal and running node -v and npm -v — both should return version numbers without errors.
A code editor like Visual Studio Code works great for this kind of project. You’ll also want a tool for testing your API endpoints. Postman is a popular GUI option, but if you prefer staying in the terminal, curl works just fine.
Setting Up Your Project
Open your terminal and create a new directory for your project. Navigate into it, then initialize a new Node.js project by running npm init -y. The -y flag accepts all the default settings, which is fine for our purposes. This creates a package.json file, which tracks your project’s metadata and dependencies.
Next, install Express:
npm install express
You’ll also want to install nodemon as a development dependency. Nodemon automatically restarts your server whenever you save changes to a file, which saves you from manually stopping and restarting the server constantly during development:
npm install –save-dev nodemon
Open your package.json file and update the scripts section to include a start command and a dev command:
json
“scripts”: {
“start”: “node index.js”,
“dev”: “nodemon index.js”
}
Now create a file called index.js in the root of your project. This will be the entry point for your application.
Building Your First Express Server
Open index.js and add the following code:
javascript
const express = require(‘express’);
const app = express();
const PORT = 3000;
app.use(express.json());
app.get(‘/’, (req, res) => {
res.send(‘Welcome to the REST API!’);
});
app.listen(PORT, () => {
console.log(Server is running on http://localhost:${PORT});
});
Let’s walk through what’s happening here. You import Express and create an instance of the application. You define a port number — 3000 is a common choice for local development. The line app.use(express.json()) is middleware that tells Express to automatically parse incoming JSON request bodies, which you’ll need when clients send data to your API. You then define a simple GET route for the root URL, and finally you tell the server to start listening for requests.
Run npm run dev in your terminal. If everything went well, you should see the confirmation message in the console. Open your browser and go to http://localhost:3000 — you should see the welcome message.
Congratulations, you have a running server. Now let’s make it do something useful.
Designing Your API: Working with a Resource
For this tutorial, we’ll build an API that manages a collection of books. This is a clean, relatable example that covers all the essential CRUD operations without adding unnecessary complexity.
Rather than connecting to a real database right now (we’ll touch on that later), we’ll use an in-memory array to store our data. This keeps the focus on understanding REST API structure without getting sidetracked by database setup.
Create a simple data structure near the top of your index.js file, after the middleware setup:
javascript
let books = [
{ id: 1, title: ‘The Great Gatsby’, author: ‘F. Scott Fitzgerald’ },
{ id: 2, title: ‘To Kill a Mockingbird’, author: ‘Harper Lee’ },
{ id: 3, title: ‘1984’, author: ‘George Orwell’ }
];
let nextId = 4;
Creating the CRUD Endpoints
GET all books
This endpoint returns the entire list of books:
javascript
app.get(‘/api/books’, (req, res) => {
res.json(books);
});
When a client sends a GET request to /api/books, your server responds with the full array as JSON. The prefix /api is a common convention that clearly signals these are API routes rather than page routes.
GET a single book by ID
javascript
app.get(‘/api/books/:id’, (req, res) => {
const id = parseInt(req.params.id);
const book = books.find(b => b.id === id);
if (!book) {
return res.status(404).json({ message: ‘Book not found’ });
}
res.json(book);
});
The :id in the route is a dynamic parameter. Express captures whatever value appears in that position in the URL and makes it available through req.params.id. If no book matches the given ID, you return a 404 status with a helpful error message. Always return appropriate HTTP status codes — they’re part of what makes a REST API well-designed and predictable.
POST: Create a new book
javascript
app.post(‘/api/books’, (req, res) => {
const { title, author } = req.body;
if (!title || !author) {
return res.status(400).json({ message: ‘Title and author are required’ });
}
const newBook = {
id: nextId++,
title,
author
};
books.push(newBook);
res.status(201).json(newBook);
});
Here you pull the title and author from the request body (remember that express.json() middleware handles parsing it for you), validate that both fields are present, create a new book object with a unique ID, add it to your array, and respond with a 201 status code, which specifically means “Created.” This is a subtle but important distinction from 200, which just means “OK.”
PUT: Update an existing book
javascript
app.put(‘/api/books/:id’, (req, res) => {
const id = parseInt(req.params.id);
const bookIndex = books.findIndex(b => b.id === id);
if (bookIndex === -1) {
return res.status(404).json({ message: ‘Book not found’ });
}
const { title, author } = req.body;
books[bookIndex] = {
id,
title: title || books[bookIndex].title,
author: author || books[bookIndex].author
};
res.json(books[bookIndex]);
});
This endpoint finds the book by ID, then replaces its properties with whatever the client sent. If a field isn’t provided, the existing value is preserved. This approach is often called a partial update pattern and makes your API more forgiving and flexible.
DELETE: Remove a book
javascript
app.delete(‘/api/books/:id’, (req, res) => {
const id = parseInt(req.params.id);
const bookIndex = books.findIndex(b => b.id === id);
if (bookIndex === -1) {
return res.status(404).json({ message: ‘Book not found’ });
}
books.splice(bookIndex, 1);
res.status(200).json({ message: ‘Book deleted successfully’ });
});
The DELETE endpoint removes the book from the array and confirms the operation. Some APIs return a 204 status with no body for deletions, which is also perfectly valid. The choice often comes down to what makes life easiest for whoever is consuming your API.
Testing Your Endpoints
Now that all five endpoints are in place, it’s time to test them. If you’re using Postman, create a new collection and add requests for each endpoint. If you prefer the command line, here are a few curl commands to get you going:
Get all books: curl http://localhost:3000/api/books
Get a single book: curl http://localhost:3000/api/books/1
Create a book: curl -X POST http://localhost:3000/api/books -H "Content-Type: application/json" -d '{"title":"Brave New World","author":"Aldous Huxley"}'
Delete a book: curl -X DELETE http://localhost:3000/api/books/2
Work through each one and make sure you’re getting the responses you expect. Try edge cases too — what happens if you request a book with an ID that doesn’t exist? What happens if you POST without a title? A well-tested API is a reliable API.
Organizing Your Code with Routers
As your API grows, keeping all your routes in a single index.js file becomes messy and hard to maintain. Express provides a feature called Router that lets you split your routes into separate files organized by resource.
Create a folder called routes and inside it, a file called books.js. Move all your book-related routes into that file, importing Express and creating a router instance:
javascript
const express = require(‘express’);
const router = express.Router();
// your book routes go here, replacing app.get with router.get, etc.
module.exports = router;
Then in your index.js, import and mount the router:
javascript
const bookRoutes = require(‘./routes/books’);
app.use(‘/api/books’, bookRoutes);
When you do this, each route inside books.js no longer needs the /api/books prefix — Express handles that automatically based on where you mount the router. So router.get('/:id', ...) will match requests to /api/books/:id. This pattern keeps each resource neatly contained and makes your project much easier to navigate as it grows.
Adding Middleware for Better Error Handling
Robust APIs handle errors gracefully. Express supports a special kind of middleware for centralized error handling — it takes four parameters instead of three, with the first being an error object:
javascript
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: ‘Something went wrong on the server’ });
});
Place this at the very end of your index.js, after all your routes. Whenever you call next(err) from anywhere in your application, Express will skip to this handler. This is much cleaner than scattering try-catch blocks throughout your code.
Where to Go from Here
What you’ve built is a genuine, functional REST API. But there’s a natural next step that most real-world projects take quickly, and that’s connecting to a persistent database. Right now, your data lives only in memory — every time the server restarts, it resets. For a production application, you’d connect to something like MongoDB with Mongoose, or a relational database like PostgreSQL with an ORM like Sequelize or Prisma.
Authentication is another critical layer. Most APIs don’t let just anyone create or delete data. JSON Web Tokens (JWT) are a popular, stateless way to handle user authentication in REST APIs, and adding them to what you’ve built here is a logical next step.
You might also want to explore environment variables — using a package like dotenv to store sensitive configuration like database connection strings and secret keys outside of your code, rather than hardcoding them. This is considered essential practice for any project you’d deploy or share publicly.
Finally, when you’re ready to deploy, platforms like Railway, Render, or Heroku make it relatively straightforward to take a Node.js application from your local machine to a live URL.
Wrapping Up
Building a REST API with Node.js and Express teaches you something fundamental about how the modern web works. Every time you use an app on your phone, interact with a service online, or log into a website, there’s almost certainly an API doing the heavy lifting in the background — receiving requests, processing data, and sending responses back in milliseconds.
The project you’ve built today covers the full CRUD cycle, proper HTTP status codes, route parameters, request body parsing, and basic project organization. These aren’t just tutorial exercises — they’re the actual patterns you’ll encounter and use in professional development work.
The best way to solidify what you’ve learned is to build something you actually care about. Pick a topic — movies, recipes, workout logs, whatever interests you — and build an API for it from scratch without looking at this tutorial. You’ll find that the concepts stick much better when they’re attached to a project that means something to you.