Skip to main content
{/}codefunc
Web & APIJuly 7, 2026 · 9 min read

REST APIs for Frontend Developers: Requests, Responses, and Error Handling

A practical guide to consuming REST APIs in frontend applications — HTTP methods, status codes, fetch patterns, authentication, error handling, and debugging techniques.

By codefunc

restapihttpfetchfrontendauthenticationerror-handling

Frontend applications talk to backends through APIs. REST (Representational State Transfer) is the dominant pattern — resources identified by URLs, actions expressed through HTTP methods, data exchanged as JSON. Understanding REST is not optional for frontend developers; it is how your UI gets data, how user actions persist, and where half your bugs will come from.

Bottom line: REST APIs use HTTP methods (GET, POST, PUT, DELETE) to perform operations on resources (URLs). Responses include status codes that tell you what happened. Handle errors gracefully — networks fail, servers error, tokens expire. Parse JSON carefully. Log enough to debug, but never log secrets.

REST fundamentals

Resources and URLs

A REST API exposes resources at URLs:

GET    /users           → List all users
GET    /users/123       → Get user 123
POST   /users           → Create a user
PUT    /users/123       → Replace user 123
PATCH  /users/123       → Update user 123 partially
DELETE /users/123       → Delete user 123

URLs are nouns (resources), HTTP methods are verbs (actions).

HTTP methods

Method Purpose Idempotent Has body
GET Read resource Yes No
POST Create resource No Yes
PUT Replace resource Yes Yes
PATCH Partial update Yes Yes
DELETE Remove resource Yes Rarely

Idempotent means calling it multiple times has the same effect as calling it once.

Request structure

POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}

Components:

  • Method + URL + Protocol
  • Headers — metadata (auth, content type, caching)
  • Body — payload for POST/PUT/PATCH

Response structure

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/456

{
  "id": "456",
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "createdAt": "2026-07-07T10:00:00Z"
}

Components:

  • Status line — protocol, status code, reason
  • Headers — metadata
  • Body — response data (usually JSON)

HTTP status codes

Status codes tell you what happened. Learn the categories:

Range Category Examples
2xx Success 200 OK, 201 Created, 204 No Content
3xx Redirection 301 Moved, 304 Not Modified
4xx Client error 400 Bad Request, 401 Unauthorized, 404 Not Found
5xx Server error 500 Internal Error, 502 Bad Gateway, 503 Unavailable

Common status codes

Code Meaning Frontend action
200 Success Use the data
201 Created Resource created, maybe redirect
204 No Content Success, no body (delete)
400 Bad Request Show validation errors
401 Unauthorized Redirect to login
403 Forbidden Show access denied
404 Not Found Show not found page
422 Unprocessable Show validation errors
429 Too Many Requests Back off, retry later
500 Server Error Show generic error, log
503 Unavailable Show maintenance message

Use the HTTP Status Code Lookup when debugging unfamiliar status codes.

Fetch API basics

Simple GET request

const response = await fetch('/api/users');

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const users = await response.json();

POST with JSON body

const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Ada Lovelace',
    email: 'ada@example.com',
  }),
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.message);
}

const user = await response.json();

Common fetch gotcha

fetch only rejects on network errors, not HTTP errors:

// This doesn't throw on 404 or 500!
const response = await fetch('/api/users/999');

// You must check response.ok or response.status
if (!response.ok) {
  // Handle error
}

Request patterns

GET with query parameters

const params = new URLSearchParams({
  page: '1',
  limit: '20',
  sort: 'createdAt',
  order: 'desc',
});

const response = await fetch(`/api/users?${params}`);

PUT vs PATCH

// PUT: Replace entire resource
await fetch('/api/users/123', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Ada Lovelace',
    email: 'ada@example.com',
    role: 'admin',
  }),
});

// PATCH: Update specific fields
await fetch('/api/users/123', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    role: 'admin',
  }),
});

DELETE request

const response = await fetch('/api/users/123', {
  method: 'DELETE',
});

if (response.status === 204) {
  // Successfully deleted, no body
}

Authentication

Bearer tokens (JWT)

const token = localStorage.getItem('accessToken');

const response = await fetch('/api/users', {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
});

For debugging JWT issues, decode tokens with the JWT Decoder to check claims like exp (expiration) and aud (audience).

Handling token expiration

async function fetchWithAuth(url, options = {}) {
  let token = getAccessToken();
  
  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`,
    },
  });
  
  if (response.status === 401) {
    // Try to refresh token
    token = await refreshAccessToken();
    
    if (token) {
      // Retry with new token
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${token}`,
        },
      });
    } else {
      // Refresh failed, redirect to login
      redirectToLogin();
    }
  }
  
  return response;
}

For same-origin requests with HttpOnly cookies:

const response = await fetch('/api/users', {
  credentials: 'include', // Send cookies
});

Error handling

Error response patterns

APIs return errors in various formats:

// Simple message
{ "error": "User not found" }

// With code
{ "code": "USER_NOT_FOUND", "message": "User not found" }

// Validation errors
{
  "errors": [
    { "field": "email", "message": "Invalid email format" },
    { "field": "password", "message": "Minimum 8 characters" }
  ]
}

Robust error handling

async function apiRequest(url, options) {
  try {
    const response = await fetch(url, options);
    
    // Parse response body
    let data;
    const contentType = response.headers.get('content-type');
    if (contentType?.includes('application/json')) {
      data = await response.json();
    } else {
      data = await response.text();
    }
    
    // Handle HTTP errors
    if (!response.ok) {
      throw new ApiError(response.status, data);
    }
    
    return data;
  } catch (error) {
    if (error instanceof ApiError) {
      throw error;
    }
    // Network error
    throw new NetworkError(error.message);
  }
}

class ApiError extends Error {
  constructor(status, data) {
    super(data.message || `HTTP ${status}`);
    this.status = status;
    this.data = data;
  }
}

class NetworkError extends Error {
  constructor(message) {
    super(message || 'Network error');
  }
}

User-friendly error messages

function getErrorMessage(error) {
  if (error instanceof NetworkError) {
    return 'Unable to connect. Check your internet connection.';
  }
  
  if (error instanceof ApiError) {
    switch (error.status) {
      case 400:
        return error.data.message || 'Invalid request';
      case 401:
        return 'Please log in to continue';
      case 403:
        return 'You don\'t have permission for this action';
      case 404:
        return 'Resource not found';
      case 429:
        return 'Too many requests. Please wait a moment.';
      case 500:
      case 502:
      case 503:
        return 'Server error. Please try again later.';
      default:
        return 'An unexpected error occurred';
    }
  }
  
  return 'An unexpected error occurred';
}

Loading and state management

Request states

type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

React pattern

function useUsers() {
  const [state, setState] = useState<RequestState<User[]>>({ status: 'idle' });
  
  async function fetchUsers() {
    setState({ status: 'loading' });
    
    try {
      const users = await apiRequest('/api/users');
      setState({ status: 'success', data: users });
    } catch (error) {
      setState({ status: 'error', error });
    }
  }
  
  return { state, fetchUsers };
}

Debugging API requests

Browser DevTools

  1. Open Network tab
  2. Filter by Fetch/XHR
  3. Click request to see:
    • Headers (request and response)
    • Payload (request body)
    • Preview/Response (response body)
    • Timing

Common issues

Symptom Likely cause
Request pending forever Server not responding, wrong URL
CORS error Backend missing CORS headers
401 on valid token Token expired, wrong header format
Empty response body Check Content-Type, parse method
Data shape mismatch API changed, check docs

Generating cURL commands

For sharing requests with backend developers or testing:

curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJ..." \
  -d '{"name": "Ada", "email": "ada@example.com"}'

Generate cURL commands from request parameters with the cURL Command Generator.

Validating JSON payloads

Before sending or after receiving:

  1. Format with the JSON Formatter to inspect structure
  2. Validate with the JSON Validator to catch syntax errors

CORS (Cross-Origin Resource Sharing)

When your frontend (app.example.com) calls an API (api.example.com), CORS rules apply.

How CORS works

  1. Browser sends preflight OPTIONS request (for non-simple requests)
  2. Server responds with allowed origins, methods, headers
  3. Browser allows or blocks the actual request

CORS errors

Access to fetch at 'https://api.example.com' from origin 'https://app.example.com' 
has been blocked by CORS policy

Fix: Backend must add CORS headers:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

For development, use a proxy or configure your dev server to forward API requests.

Data fetching libraries

When to use fetch directly

  • Simple requests
  • Full control needed
  • Avoiding dependencies

When to use a library

Library Use case
TanStack Query Caching, background refetch, optimistic updates
SWR Similar to TanStack Query, smaller
Axios Request/response interceptors, better defaults
ky Fetch wrapper with retries, simpler API

TanStack Query example

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => apiRequest('/api/users'),
  });
}

function useCreateUser() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (data) => apiRequest('/api/users', {
      method: 'POST',
      body: JSON.stringify(data),
    }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

API versioning

APIs evolve. Versioning prevents breaking changes:

/api/v1/users
/api/v2/users

Or via headers:

Accept: application/vnd.api+json; version=2

Check API documentation for versioning strategy.

Rate limiting

APIs limit requests to prevent abuse:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1625000000

Handle gracefully:

if (response.status === 429) {
  const retryAfter = response.headers.get('Retry-After');
  await delay(parseInt(retryAfter) * 1000);
  return retry(request);
}

Practical takeaway

REST APIs are HTTP with conventions. Use the right method for the action. Check status codes, don't assume success. Parse JSON safely. Handle errors with user-friendly messages. Use DevTools Network tab to debug. Cache and deduplicate requests in complex UIs. Understand CORS before blaming the backend. Document your API contracts — TypeScript interfaces are living documentation.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool