REST API Basics
Table of Contents
APIs (Application Programming Interfaces) are how software systems talk to each other. When you check the weather on your phone, scroll through social media, or make an online payment, your app is communicating with a server through an API. REST is the most common architectural style for web APIs, and understanding it is essential for any developer.
This tutorial covers what REST is, how HTTP methods map to operations, and how to work with REST APIs using JavaScript.
What Is REST?
REST (Representational State Transfer) is a set of conventions for building web APIs. A REST API exposes resources (data objects like users, posts, or products) at specific URLs, and you interact with them using standard HTTP methods.
Key principles:
- Resources have URLs — Each thing you can interact with has a unique address
- Standard HTTP methods — GET to read, POST to create, PUT/PATCH to update, DELETE to remove
- Stateless — Each request contains all the information needed; the server doesn’t remember previous requests
- JSON responses — Most modern APIs return data as JSON
HTTP Methods
| Method | Purpose | Example |
|---|---|---|
| GET | Read/retrieve data | Get a list of users |
| POST | Create new data | Create a new user |
| PUT | Replace existing data entirely | Replace a user’s profile |
| PATCH | Partially update data | Update just a user’s email |
| DELETE | Remove data | Delete a user |
URL Structure
REST APIs follow predictable URL patterns:
GET /api/users → List all users
GET /api/users/42 → Get user with ID 42
POST /api/users → Create a new user
PUT /api/users/42 → Replace user 42
PATCH /api/users/42 → Update user 42 partially
DELETE /api/users/42 → Delete user 42
Nested resources:
GET /api/users/42/posts → Get all posts by user 42
POST /api/users/42/posts → Create a post for user 42
GET /api/users/42/posts/7 → Get post 7 by user 42
Making API Requests with fetch
JavaScript’s fetch API is the standard way to make HTTP requests in the browser:
GET — Reading data
async function getUsers() {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const users = await response.json();
console.log(users);
return users;
}
POST — Creating data
async function createUser(userData) {
const response = await fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const newUser = await response.json();
console.log("Created:", newUser);
return newUser;
}
createUser({ name: "Alice", email: "alice@example.com" });
PUT — Replacing data
async function replaceUser(id, userData) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData)
});
return response.json();
}
PATCH — Partial update
async function updateEmail(id, newEmail) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: newEmail })
});
return response.json();
}
DELETE — Removing data
async function deleteUser(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
method: "DELETE"
});
if (!response.ok) {
throw new Error(`Failed to delete user ${id}`);
}
console.log(`User ${id} deleted`);
}
HTTP Status Codes
The server communicates success or failure through status codes:
| Code | Meaning | When You’ll See It |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE (no body returned) |
| 400 | Bad Request | Invalid data sent |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not allowed |
| 404 | Not Found | Resource doesn’t exist |
| 422 | Unprocessable Entity | Validation errors |
| 429 | Too Many Requests | Rate limited |
| 500 | Internal Server Error | Server-side bug |
Always check response.ok (true for 200–299 status codes) before processing the response body.
Query Parameters
Use query parameters for filtering, sorting, and pagination:
GET /api/users?role=admin → Filter by role
GET /api/users?sort=name&order=asc → Sort by name ascending
GET /api/users?page=2&limit=20 → Page 2, 20 items per page
GET /api/posts?userId=42&status=published → Multiple filters
In JavaScript:
async function searchUsers(filters) {
const params = new URLSearchParams(filters);
const response = await fetch(`/api/users?${params}`);
return response.json();
}
searchUsers({ role: "admin", sort: "name", limit: "10" });
// Fetches: /api/users?role=admin&sort=name&limit=10
Request and Response Headers
Common request headers
const response = await fetch("/api/data", {
headers: {
"Content-Type": "application/json", // what you're sending
"Accept": "application/json", // what you want back
"Authorization": "Bearer eyJhbG..." // authentication token
}
});
Reading response headers
const response = await fetch("/api/users");
console.log(response.headers.get("Content-Type"));
console.log(response.headers.get("X-Total-Count")); // custom header for total items
Authentication
Most APIs require authentication. Common approaches:
API Key (simple, less secure)
// As a query parameter
fetch("/api/data?api_key=your-key-here");
// As a header (preferred)
fetch("/api/data", {
headers: { "X-API-Key": "your-key-here" }
});
Bearer Token (JWT)
const token = "eyJhbGciOiJIUzI1NiIs...";
fetch("/api/protected-resource", {
headers: { "Authorization": `Bearer ${token}` }
});
Error Handling
A robust API client handles errors gracefully:
async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, {
headers: { "Content-Type": "application/json" },
...options
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new Error(errorBody.message || `HTTP ${response.status}`);
}
// Handle 204 No Content
if (response.status === 204) return null;
return await response.json();
} catch (error) {
if (error.name === "TypeError") {
// Network error (no internet, DNS failure, etc.)
throw new Error("Network error — check your connection");
}
throw error;
}
}
Pagination
APIs return large datasets in pages. Common patterns:
Offset-based
async function getAllUsers() {
const allUsers = [];
let page = 1;
const limit = 50;
while (true) {
const users = await apiRequest(`/api/users?page=${page}&limit=${limit}`);
allUsers.push(...users);
if (users.length < limit) break; // last page
page++;
}
return allUsers;
}
Cursor-based
async function getAllPosts() {
const allPosts = [];
let cursor = null;
while (true) {
const url = cursor ? `/api/posts?cursor=${cursor}` : "/api/posts";
const { data, nextCursor } = await apiRequest(url);
allPosts.push(...data);
if (!nextCursor) break;
cursor = nextCursor;
}
return allPosts;
}
Testing APIs
Before writing code, test APIs with tools:
- Browser DevTools (Network tab) — see requests your page makes
- curl (command line) — quick tests from the terminal
- Postman / Insomnia — GUI tools for building and saving requests
# GET request
curl https://jsonplaceholder.typicode.com/users/1
# POST request
curl -X POST https://jsonplaceholder.typicode.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
REST Best Practices
When consuming APIs:
- Always check status codes before processing responses
- Handle network errors (offline, timeouts)
- Respect rate limits (check for 429 responses and
Retry-Afterheaders) - Don’t hardcode URLs — use a base URL variable
When designing APIs:
- Use nouns for URLs (
/users), not verbs (/getUsers) - Use plural nouns (
/users, not/user) - Return appropriate status codes
- Include pagination for list endpoints
- Version your API (
/api/v1/users)
What’s Next
Now that you understand REST APIs, you can build your own with Node.js and Express, or explore how to handle the asynchronous nature of API calls with Promises and Async/Await.