Understanding REST APIs in Node.js: A Practical Guide
Understand how modern apps communicate using REST principles, HTTP methods, and clean API design — with real Node.js code examples.

Hi, I’m Abdul Samad. A web development learner and tech enthusiast. I write about what I learn, share practical coding tips, and publish in-depth blogs on programming and modern web development.
Check out my full collection of blogs on Hashnode: https://abdulsamad30.hashnode.dev/
Connect with me on X for quick updates and insights: @abdul_sama60108
Every time you use a mobile app, visit a website, or log into an account, your device is talking to a server behind the scenes. That conversation follows rules and one of the most common sets of rules for that conversation is called REST.
If you've heard the term "REST API" thrown around and felt unsure about what it actually means, you're not alone. It's one of those concepts that sounds more complicated than it really is. By the end of this post, you'll not only understand REST APIs you'll be able to design and explain them confidently.
What REST API Means
Let's break this into two parts.
An API (Application Programming Interface) is a way for two pieces of software to communicate with each other. When your frontend app needs user data, it doesn't dig into the database directly it sends a request to an API, and the API sends back the data. Think of an API as a waiter in a restaurant. You don't walk into the kitchen yourself you tell the waiter what you want, the waiter goes to the kitchen, and brings back your food.
REST (Representational State Transfer) is a set of rules and conventions for designing that API. It's not a technology or a library it's an architectural style. When an API follows REST principles, it's called a RESTful API.
A REST API communicates over HTTP the same protocol your browser uses to load web pages. The client (your app, browser, or any consumer) sends an HTTP request to the server, and the server sends back an HTTP response. That request-response cycle is the foundation of every REST API.
const http = require('http');
const server = http.createServer(function (req, res) {
console.log(`Incoming request: \({req.method} \){req.url}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringi({
message: "Hello from the REST API",
status: "success"
}));
});
server.listen(3000, function () {
console.log('REST API server running on <http://localhost:3000>');
});
Expected Output (when a request is made):
REST API server running on <http://localhost:3000>
Incoming request: GET /
Response received by the client:
{
"message": "Hello from the REST API",
"status": "success"
}
That's the basic shape of every REST API interaction a request goes in, a response comes out, and both follow HTTP conventions.
Resources in REST Architecture
REST is built around the concept of resources. A resource is any piece of data or entity that your API manages. In most applications, resources map directly to the things your app deals with users, products, orders, articles, comments.
Each resource is identified by a unique URL (called an endpoint). This is what makes REST intuitive the URL tells you what you're working with.
For example, if your resource is users:
/usersrepresents the collection of all users/users/42represents a specific user with ID 42
The URL structure is intentionally clean and predictable. You don't need to guess what /users/42 means it's user number 42. That clarity is a core principle of REST.
const http = require('http');
// Our users resource a simple array acting as a data store
const users = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
{ id: 3, name: "Charlie", email: "charlie@example.com" }
];
const server = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'application/json');
if (req.url === '/users' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify(users));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: "Resource not found" }));
}
});
server.listen(3000, function () {
console.log('API running on <http://localhost:3000>');
});
Expected Output (requesting GET /users):
[
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" },
{ "id": 3, "name": "Charlie", "email": "charlie@example.com" }
]
The /users endpoint represents the users resource. Simple, clean, and anyone reading the URL knows exactly what it returns.
HTTP Methods: GET, POST, PUT, DELETE
In REST, you don't use special keywords or custom actions to tell the server what to do. You use HTTP methods also called verbs. Each method represents a type of action you want to perform on a resource.
There are four fundamental ones:
GET Read Data
GET retrieves data from the server. It never changes anything it's a read-only operation. When you visit a webpage, your browser sends a GET request.
// GET request handler fetch all users
if (req.url === '/users' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify(users));
}
Expected Output (GET /users):
[
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
]
POST Create Data
POST sends new data to the server to create a new resource. When you fill out a signup form and hit submit, that's typically a POST request.
// POST request handler create a new user
if (req.url === '/users' && req.method === 'POST') {
let body = '';
req.on('data', function (chunk) {
body += chunk;
});
req.on('end', function () {
const newUser = JSON.parse(body);
newUser.id = users.length + 1;
users.push(newUser);
res.writeHead(201);
res.end(JSON.stringify(newUser));
});
}
Expected Output (POST /users with body: {"name":"Diana","email":"diana@example.com"}):
{ "name": "Diana", "email": "diana@example.com", "id": 4 }
PUT Update Data
PUT replaces an existing resource with new data. You send the complete updated version of the resource to the server. It's like telling the server: "here's the new version of this thing use this instead."
// PUT request handler update a user by ID
if (req.url.startsWith('/users/') && req.method === 'PUT') {
const userId = parseInt(req.url.split('/')[2]);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
res.writeHead(404);
res.end(JSON.stringify({ error: "User not found" }));
return;
}
let body = '';
req.on('data', function (chunk) {
body += chunk;
});
req.on('end', function () {
const updatedData = JSON.parse(body);
users[userIndex] = { id: userId, ...updatedData };
res.writeHead(200);
res.end(JSON.stringify(users[userIndex]));
});
}
Expected Output (PUT /users/2 with body: {"name":"Bobby","email":"bobby@example.com"}):
{ "id": 2, "name": "Bobby", "email": "bobby@example.com" }
DELETE Remove Data
DELETE removes a resource from the server. Send a DELETE request to a specific resource URL, and it's gone.
// DELETE request handler remove a user by ID
if (req.url.startsWith('/users/') && req.method === 'DELETE') {
const userId = parseInt(req.url.split('/')[2]);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
res.writeHead(404);
res.end(JSON.stringify({ error: "User not found" }));
return;
}
users.splice(userIndex, 1);
res.writeHead(200);
res.end(JSON.stringify({ message: `User ${userId} deleted successfully` }));
}
Expected Output (DELETE /users/2):
{ "message": "User 2 deleted successfully" }
These four methods map directly to the four basic operations you can perform on data Create, Read, Update, Delete commonly known as CRUD.
Status Codes Basics
Every REST API response includes a status code a three-digit number that tells the client what happened with their request. You don't need to memorize hundreds of them. A handful covers the vast majority of situations.
Here are the ones that matter most:
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Request succeeded, data returned |
| 201 | Created | New resource was created successfully |
| 400 | Bad Request | Client sent invalid data |
| 404 | Not Found | Requested resource doesn't exist |
| 500 | Server Error | Something went wrong on the server |
The first digit tells you the category. 2xx means success, 4xx means the client made a mistake, 5xx means the server had a problem.
const http = require('http');
const users = [
{ id: 1, name: "Alice", email: "alice@example.com" }
];
const server = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'application/json');
// GET /users Success: 200
if (req.url === '/users' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify(users));
return;
}
// GET /users/999 Not Found: 404
if (req.url === '/users/999' && req.method === 'GET') {
res.writeHead(404);
res.end(JSON.stringify({ error: "User not found" }));
return;
}
// Anything else Bad Request: 400
res.writeHead(400);
res.end(JSON.stringify({ error: "Invalid request" }));
});
server.listen(3000, function () {
console.log('Server running on port 3000');
});
Expected Output (GET /users):
[{ "id": 1, "name": "Alice", "email": "alice@example.com" }]
Status: 200 OK
Expected Output (GET /users/999):
{ "error": "User not found" }
Status: 404 Not Found
Status codes aren't just decoration they tell the client how to interpret the response. A well-designed API always returns the right status code for the right situation.
Designing Routes Using REST Principles
Route design is where everything comes together. A well-designed REST API is predictable you should be able to guess the endpoint for a resource without reading documentation.
The convention is straightforward:
Use nouns for resource names (
/users, not/getUsers)Use plural nouns for collections (
/users, not/user)Use the HTTP method to indicate the action, not the URL
Use IDs in the URL for specific resources (
/users/42)
Here's the complete route structure for our users resource:
| Method | Route | Action |
|---|---|---|
| GET | /users |
Get all users |
| GET | /users/:id |
Get one user |
| POST | /users |
Create a new user |
| PUT | /users/:id |
Update a user |
| DELETE | /users/:id |
Delete a user |
Let's put the complete server together:
const http = require('http');
let users = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" }
];
let nextId = 3;
const server = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'application/json');
// GET /users
if (req.url === '/users' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify(users));
return;
}
// GET /users/:id
if (req.url.startsWith('/users/') && req.method === 'GET') {
const id = parseInt(req.url.split('/')[2]);
const user = users.find(u => u.id === id);
if (!user) {
res.writeHead(404);
res.end(JSON.stringify({ error: "User not found" }));
return;
}
res.writeHead(200);
res.end(JSON.stringify(user));
return;
}
// POST /users
if (req.url === '/users' && req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', function () {
const data = JSON.parse(body);
const newUser = { id: nextId++, name: data.name, email: data.email };
users.push(newUser);
res.writeHead(201);
res.end(JSON.stringify(newUser));
});
return;
}
// PUT /users/:id
if (req.url.startsWith('/users/') && req.method === 'PUT') {
const id = parseInt(req.url.split('/')[2]);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
res.writeHead(404);
res.end(JSON.stringify({ error: "User not found" }));
return;
}
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', function () {
const data = JSON.parse(body);
users[index] = { id, name: data.name, email: data.email };
res.writeHead(200);
res.end(JSON.stringify(users[index]));
});
return;
}
// DELETE /users/:id
if (req.url.startsWith('/users/') && req.method === 'DELETE') {
const id = parseInt(req.url.split('/')[2]);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
res.writeHead(404);
res.end(JSON.stringify({ error: "User not found" }));
return;
}
users.splice(index, 1);
res.writeHead(200);
res.end(JSON.stringify({ message: `User ${id} deleted` }));
return;
}
// Unknown route
res.writeHead(400);
res.end(JSON.stringify({ error: "Invalid request" }));
});
server.listen(3000, function () {
console.log('REST API running on <http://localhost:3000>');
console.log('Try: GET /users, GET /users/1, POST /users, PUT /users/1, DELETE /users/1');
});
Expected Output (terminal):
REST API running on <http://localhost:3000>
Try: GET /users, GET /users/1, POST /users, PUT /users/1, DELETE /users/1
Testing each endpoint:
| Request | Response |
|---|---|
GET /users |
200 Array of all users |
GET /users/1 |
200 Alice's data |
GET /users/99 |
404 User not found |
POST /users with body |
201 Newly created user |
PUT /users/1 with body |
200 Updated user data |
DELETE /users/2 |
200 Deletion confirmation |
Clean URLs, correct HTTP methods, proper status codes, and all CRUD operations working on a single resource. This is a complete, functional REST API built from scratch using nothing but Node.js's built-in modules.
Wrapping Up
REST APIs aren't a framework or a library they're a way of thinking about how clients and servers communicate. Once the patterns click, designing APIs becomes intuitive. Let's recap:
REST APIs use HTTP to let clients and servers communicate through structured requests and responses.
Resources are the nouns of your API
/users,/orders,/productsidentified by clean, predictable URLs.HTTP methods define the actions GET reads, POST creates, PUT updates, DELETE removes.
Status codes tell the client what happened 200 for success, 201 for creation, 404 for not found, 400 for bad requests.
Route design follows conventions plural nouns, HTTP methods for actions, IDs for specific resources.
The users resource example covered here is a template that works for any resource in any application. Add an orders resource, a products resource, or anything else the same REST principles apply identically. That consistency is exactly what makes REST so widely adopted and so reliable to build with.
FIN ✌️




