TL;DR
- Use
fetch()for HTTP requests,response.json()to parse - Set
Content-Type: application/jsonfor POST/PUT requests - Always handle errors — network failures, 4xx/5xx responses
- REST APIs use HTTP methods: GET (read), POST (create), PUT (update), DELETE
- Use
async/awaitfor cleaner async code
What's a JSON API?
An API (Application Programming Interface) is how programs talk to each other. A JSON API is an API that sends and receives data in JSON format.
When you:
- Load Twitter — your browser fetches tweets via JSON API
- Check weather on your phone — the app calls a weather JSON API
- Process payments — your backend talks to Stripe's JSON API
JSON APIs are everywhere. Let's learn to use them.
Making HTTP Requests
The modern way to call APIs in JavaScript is the fetch() function.
It's built into all browsers and Node.js 18+.
Basic GET Request
// Fetch data from an API
const response = await fetch('https://api.example.com/users');
const users = await response.json();
console.log(users);
// [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
That's it. Two lines. fetch() makes the request,
.json() parses the response.
Full Example with Error Handling
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
// Check if request was successful
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error);
throw error;
}
}
// Use it
const users = await getUsers(); fetch() doesn't throw on 404 or 500 errors!
You must check response.ok or response.status yourself.
HTTP Methods (CRUD Operations)
REST APIs use different HTTP methods for different operations:
| Method | Purpose | Request Body |
|---|---|---|
GET | Read/fetch data | No |
POST | Create new resource | Yes (JSON) |
PUT | Update entire resource | Yes (JSON) |
PATCH | Partial update | Yes (JSON) |
DELETE | Remove resource | Usually no |
POST Request (Create)
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error(`Failed to create user: ${response.status}`);
}
return response.json();
}
// Use it
const newUser = await createUser({
name: 'Alice',
email: 'alice@example.com'
});
console.log(newUser.id); // Server-assigned ID PUT Request (Update)
async function updateUser(id, userData) {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error(`Failed to update user: ${response.status}`);
}
return response.json();
}
// Replace entire user data
await updateUser(123, {
name: 'Alice Smith',
email: 'alice.smith@example.com',
role: 'admin'
}); DELETE Request
async function deleteUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`Failed to delete user: ${response.status}`);
}
// DELETE often returns 204 No Content (empty body)
return response.status === 204 ? null : response.json();
}
await deleteUser(123); Request Headers
Headers provide metadata about your request:
const response = await fetch('https://api.example.com/data', {
headers: {
// Required for JSON bodies
'Content-Type': 'application/json',
// Authentication (common patterns)
'Authorization': 'Bearer your-jwt-token',
'X-API-Key': 'your-api-key',
// Accept specific response format
'Accept': 'application/json',
// Custom headers (API-specific)
'X-Request-ID': 'unique-request-id'
}
}); Common Headers
| Header | Purpose |
|---|---|
Content-Type | Format of request body (application/json) |
Accept | Expected response format |
Authorization | Authentication credentials |
X-API-Key | API key authentication |
Proper Error Handling
APIs fail. Networks drop. Servers error. Handle it gracefully:
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);
// Handle HTTP errors
if (!response.ok) {
// Try to get error message from response body
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
// Response body wasn't JSON
}
throw new Error(errorMessage);
}
// Handle empty responses
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return null;
}
return response.json();
} catch (error) {
if (error.name === 'TypeError') {
// Network error (no connection, CORS, etc.)
throw new Error('Network error: Could not connect to server');
}
throw error;
}
}
// Usage with specific error handling
try {
const data = await fetchWithErrorHandling('/api/users');
} catch (error) {
if (error.message.includes('401')) {
// Unauthorized - redirect to login
} else if (error.message.includes('404')) {
// Not found
} else if (error.message.includes('Network error')) {
// Show offline message
} else {
// Generic error
}
} Common API Response Patterns
Pattern 1: Wrapped Response
{
"status": "success",
"data": {
"users": [...]
},
"meta": {
"page": 1,
"total": 100
}
} Pattern 2: Direct Data
[
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
] Pattern 3: Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is invalid",
"details": [
{ "field": "email", "message": "Must be a valid email address" }
]
}
} Query Parameters
Filter, sort, and paginate with query strings:
// Build URL with query parameters
const params = new URLSearchParams({
page: 1,
limit: 10,
sort: 'createdAt',
order: 'desc',
status: 'active'
});
const url = `https://api.example.com/users?${params}`;
// https://api.example.com/users?page=1&limit=10&sort=createdAt&order=desc&status=active
const response = await fetch(url); Authentication
Bearer Token (JWT)
// Store token after login
const token = 'eyJhbGciOiJIUzI1NiIs...';
// Include in every request
const response = await fetch('https://api.example.com/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
}); API Key
// In header
const response = await fetch('https://api.example.com/data', {
headers: {
'X-API-Key': 'your-api-key'
}
});
// Or in query string (less secure)
const response = await fetch('https://api.example.com/data?api_key=your-key'); Building a Reusable API Client
For real projects, wrap fetch in a reusable client:
class ApiClient {
constructor(baseUrl, options = {}) {
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json',
...options.headers
};
}
setAuthToken(token) {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
...this.defaultHeaders,
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
if (response.status === 204) return null;
return response.json();
}
get(endpoint) {
return this.request(endpoint);
}
post(endpoint, data) {
return this.request(endpoint, { method: 'POST', body: data });
}
put(endpoint, data) {
return this.request(endpoint, { method: 'PUT', body: data });
}
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new ApiClient('https://api.example.com');
api.setAuthToken('your-jwt-token');
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Alice' });
await api.delete('/users/123'); Real-World Example: Todo App
const API_URL = 'https://api.example.com';
// Todo API functions
const todoApi = {
// Get all todos
async getAll() {
const response = await fetch(`${API_URL}/todos`);
if (!response.ok) throw new Error('Failed to fetch todos');
return response.json();
},
// Create a new todo
async create(text) {
const response = await fetch(`${API_URL}/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, completed: false })
});
if (!response.ok) throw new Error('Failed to create todo');
return response.json();
},
// Toggle todo completion
async toggle(id, completed) {
const response = await fetch(`${API_URL}/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed })
});
if (!response.ok) throw new Error('Failed to update todo');
return response.json();
},
// Delete a todo
async delete(id) {
const response = await fetch(`${API_URL}/todos/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete todo');
}
};
// Usage
const todos = await todoApi.getAll();
const newTodo = await todoApi.create('Learn JSON APIs');
await todoApi.toggle(newTodo.id, true);
await todoApi.delete(newTodo.id); Best Practices
- Always handle errors — Network issues, HTTP errors, invalid JSON
- Use async/await — Cleaner than .then() chains
- Set appropriate timeouts — Don't wait forever for slow APIs
- Validate responses — Check data structure matches expectations
- Log errors properly — Include URL, status, and error message
- Use environment variables — Don't hardcode API URLs or keys
- Cache when appropriate — Reduce unnecessary requests
FAQ
What about axios?
Axios is a popular alternative to fetch. It has nicer syntax and automatic JSON parsing. Both work great — fetch is built-in, axios is a library.
How do I handle CORS errors?
CORS errors happen when the API server doesn't allow your domain. Either: 1) The API needs to add CORS headers, 2) Use a proxy, or 3) Make requests from your backend.
Should I use GraphQL instead of REST?
REST with JSON is simpler and more widely supported. GraphQL is great for complex data requirements. Start with REST, consider GraphQL if you outgrow it.
Related Articles
- Parse JSON in JavaScript — Deep dive into parsing
- Common JSON Errors — Debug API response issues
- JSON Schema — Validate API responses