APIs 14 min read

Working with JSON APIs

From fetching data to handling responses — everything you need to know about consuming and building JSON APIs with practical examples.

#api #rest #fetch #http

TL;DR

  • Use fetch() for HTTP requests, response.json() to parse
  • Set Content-Type: application/json for 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/await for 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

basic-get.js
javascript
// 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

fetch-complete.js
javascript
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();
Important: 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)

post-request.js
javascript
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)

put-request.js
javascript
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

delete-request.js
javascript
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:

headers.js
javascript
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:

error-handling.js
javascript
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

wrapped-response.json
json
{
  "status": "success",
  "data": {
    "users": [...]
  },
  "meta": {
    "page": 1,
    "total": 100
  }
}

Pattern 2: Direct Data

direct-response.json
json
[
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]

Pattern 3: Error Response

error-response.json
json
{
  "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:

query-params.js
javascript
// 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)

bearer-auth.js
javascript
// 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

api-key-auth.js
javascript
// 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:

api-client.js
javascript
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

todo-api.js
javascript
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);
Test your API responses! Use our JSON tools to format and validate API responses while debugging.

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.

About the Author

AT

Adam Tse

Founder & Lead Developer · 10+ years experience

Full-stack engineer with 10+ years of experience building developer tools and APIs. Previously worked on data infrastructure at scale, processing billions of JSON documents daily. Passionate about creating privacy-first tools that don't compromise on functionality.

JavaScript/TypeScript Web Performance Developer Tools Data Processing