Skip to main content
⚡ Calmops

HTTP & REST Basics — A Beginner's Guide to APIs

APIs (Application Programming Interfaces) power most of today’s web. Every time your browser talks to a service—the weather app, a news feed, or a login procedure—HTTP is the language it uses, and REST (Representational State Transfer) is the common pattern for designing these APIs.

This guide explains the core building blocks: the essential HTTP verbs (GET, POST, PUT, DELETE), common HTTP status codes you’ll see, the core ideas behind REST, and practical, beginner-friendly strategies for consuming REST APIs in web apps.


Why HTTP matters for web developers

  • HTTP is the protocol used by browsers and servers to exchange information.
  • RESTful APIs make it easier to build predictable, resource-oriented web services.
  • Understanding HTTP & REST helps you design requests, handle errors, optimize performance, and secure client-server communication.
  • Understanding HTTP & REST helps you design requests, handle errors, optimize performance, and secure client-server communication.

Core terms & abbreviations

  • HTTP — HyperText Transfer Protocol. The application-layer protocol browsers and servers use to exchange data.
  • URI / URL — Uniform Resource Identifier / Uniform Resource Locator — a way to locate and identify resources on the network (e.g., https://example.com/api/users/123).
  • API — Application Programming Interface: the programmatic interface a service exposes for other programs/clients to consume.
  • REST — Representational State Transfer: resource-oriented API design principles (statelessness, explicit use of HTTP methods, etc.).
  • JSON — JavaScript Object Notation: the most common payload format for APIs.
  • CORS — Cross-Origin Resource Sharing: a browser mechanism that restricts cross-origin requests unless allowed by server headers.
  • CRUD — Create, Read, Update, Delete — common mapping to POST/GET/PUT/PATCH/DELETE.
  • Idempotent — An operation that can be called many times without changing the result beyond the initial application (PUT and GET should be idempotent).
  • JWT — JSON Web Token: a compact token format for authentication payloads.
  • ETag — An identifier that represents the current version of a resource for caching/conditional requests.

These terms will help you read docs faster and better understand sample code you’ll encounter.


Core HTTP Methods (Verbs)

HTTP verbs describe what you want to do with a resource. Servers often map verbs to operations on data.

  • GET — Retrieve a resource. Used for reading data.
  • POST — Create a resource or submit data. Often used for form submissions or creating new records.
  • PUT — Replace or update an entire resource.
  • PATCH — Apply partial updates to a resource (smaller changes than PUT).
  • DELETE — Remove a resource.

When to use each verb

  • Use GET for requests that fetch data. They should be safe and idempotent (calling them multiple times has the same effect).
  • Use POST to create a new resource or submit data that will change the server’s state.
  • Use PUT to replace a resource entirely (often idempotent).
  • Use PATCH to update just a few fields within a resource.
  • Use DELETE to remove resources.

Examples (fetch API)

GET example:

// Fetch a list of posts
fetch('/api/posts')
  .then(res => res.json())
  .then(posts => console.log(posts))
  .catch(err => console.error(err));

POST example (create):

// Create a new post
fetch('/api/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'New Post', body: 'Hello World' })
})
  .then(res => res.json())
  .then(post => console.log('Created', post))
  .catch(err => console.error(err));

PUT / PATCH example (update):

// Replace a resource with PUT
fetch('/api/posts/123', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: 123, title: 'Updated', body: 'Replaced content' })
});

// Partial update using PATCH
fetch('/api/posts/123', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Updated title' })
});

DELETE example:

fetch('/api/posts/123', { method: 'DELETE' })
  .then(res => {
    if (res.ok) console.log('Deleted');
  });

Useful fetch patterns

// GET with query params
async function fetchPosts(page = 1, perPage = 10) {
  const q = new URLSearchParams({ page, per_page: perPage });
  const res = await fetch(`/api/posts?${q}`);
  if (!res.ok) throw new Error(res.statusText);
  return res.json();
}

// POST with JSON and authorization
async function createPost(token, payload) {
  const res = await fetch('/api/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify(payload),
  });
  if (!res.ok) throw new Error('Failed to create');
  return res.json();
}

// Using AbortController to cancel a request (e.g., user navigates away)
const controller = new AbortController();
fetch('/api/posts', { signal: controller.signal })
  .then(r => r.json()).catch(e => { if (e.name !== 'AbortError') throw e; });
// cancel if necessary
controller.abort();

// Conditional requests using ETag
async function fetchIfModified(url, etag) {
  const res = await fetch(url, { headers: { 'If-None-Match': etag } });
  if (res.status === 304) return null; // Not modified
  return res.json();
}

Common HTTP Status Codes and Their Meanings

Status codes tell you how a request was processed.

  • 200 OK — Request succeeded, and returned data (GET, POST, etc.)
  • 201 Created — Request succeeded and a new resource was created (commonly returned by POST)
  • 204 No Content — Request succeeded, but there is no data to return (e.g., successful DELETE)

It’s common to return JSON objects with a consistent shape for errors so clients can handle them predictably:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "The requested user ID does not exist",
    "status": 404
  }
}

This gives you a machine-readable code and a human-friendly message.

Prevent duplicate POSTs with idempotency

If your POST creates server-side resources (e.g., charges or orders), use an Idempotency-Key header or a client-generated UUID to prevent duplicate operations if a request is retried:

async function createOrder(order, token, idempotencyKey) {
  const res = await fetch('/api/orders', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
      'Idempotency-Key': idempotencyKey
    },
    body: JSON.stringify(order),
  });
  if (!res.ok) throw res;
  return res.json();
}
  • 400 Bad Request — The request is malformed or invalid.
  • 401 Unauthorized — Authentication is required (often means missing or invalid token).
  • 403 Forbidden — Authenticated but not allowed to perform this action.
  • 404 Not Found — The requested resource does not exist.
  • 429 Too Many Requests — Rate limiting—client should slow down requests.
  • 500 Internal Server Error — Something went wrong on the server.

Useful HTTP headers (client & server)

  • Content-Type — Describes the type of the body (e.g., application/json). Servers use it to parse incoming payloads.
  • Accept — Tells the server what content types the client can accept (e.g., application/json).
  • Authorization — Contains credentials, for example Bearer <token>.
  • Cache-Control — Controls caching policies (e.g., max-age=3600, no-cache).
  • ETag — Entity tag for a resource; used for conditional requests (If-None-Match).
  • Retry-After — Used with 429 or 503 responses to indicate when to retry.
  • X-Request-ID or Request-Id — A unique ID inserted to trace a request across services for debugging/observability.

Choosing the right success code

  • 200 OK — When returning a payload associated to the request.
  • 201 Created — When a new resource was created (return the Location header pointing to the new resource where possible).
  • 202 Accepted — When the request is accepted for processing but not yet completed (e.g., background jobs).
  • 204 No Content — When the response has no body (e.g., successful delete).

How to handle status codes in your UI

  • Treat 200/201/204 as success and display the results or update the UI.
  • On 401 Unauthorized, redirect to login or refresh tokens.
  • On 403 Forbidden, show a permissions error or restricted UI state.
  • On 404 Not Found, show a 404 page or helpful message.
  • On 429 (rate limit), back off and retry later or show a warning.
  • On 5xx errors, report them to your logging or metrics service and try again cautiously.

Example: generic fetch handler that checks status codes

async function request(url, options = {}) {
  const res = await fetch(url, options);
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`${res.status} - ${res.statusText}: ${errorText}`);
  }
  return res.status !== 204 ? res.json() : null;
}

// Usage
request('/api/posts')
  .then(posts => console.log(posts))
  .catch(err => console.error('API error', err));

Fundamental REST Principles

REST is an architectural style rather than a strict protocol. It encourages a resource-oriented approach to designing APIs.

  • Resources: Everything is a resource and identified by URIs (e.g., /api/users/123).
  • HTTP verbs (GET, POST, PUT, DELETE): Use them semantically for CRUD operations.
  • Statelessness: Each request contains all information the server needs to process it; the server doesn’t remember client state between requests.
  • Client-server separation: The UI (client) and the data/service (server) are decoupled—this allows independent evolution.
  • Cacheable responses: Use HTTP caching headers so clients and proxies can cache responses safely.
  • Hypermedia (optional): Responses may include links to related resources (HATEOAS), but this is less commonly implemented in modern APIs.

Why statelessness matters:

  • Simpler server design and easier scaling.
  • Each request is self-sufficient, enabling load balancing and fault-tolerant deployments.

Practical Strategies for Consuming REST APIs

Here are practical, actionable tips for novice web developers when building a client that consumes REST APIs.

1) Use the right method & URL

  • Read the API docs to pick the correct HTTP method for each endpoint.
  • Use resource-style URLs: /api/users, /api/users/123, /api/users/123/posts.

Versioning your API

APIs should include a version strategy to prevent breaking clients when making changes. Example patterns:

  • URL versioning: /api/v1/users/123 (simple and common)
  • Header versioning: Accept: application/vnd.myapp.v1+json (keeps URLs clean)

Choose a versioning strategy that fits your release cadence and consumer needs.

2) Set appropriate headers

  • Content-Type: inform server about the data you send (commonly application/json).
  • Accept: tell the server what representation the client expects (e.g., application/json).
  • Authorization: include Bearer <token> for protected routes.

Content negotiation

Use Accept to request specific formats (JSON, XML, or vendor-specific versions):

curl -H "Accept: application/json" https://api.example.com/users/1
fetch('/api/profile', {
  method: 'GET',
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer ' + token
  }
});

3) Handle CORS gracefully

  • Browsers block requests to different origins unless the server allows it via CORS headers (e.g., Access-Control-Allow-Origin).
  • For development, configure a local proxy or enable CORS on the server.

CORS & preflight (deep dive)

  • Browsers restrict cross-origin requests for security. A fetch from https://example.com to https://api.other.com will only succeed if the target responds with the appropriate Access-Control-Allow-* headers.
  • For simple GET/POSTs the server can set Access-Control-Allow-Origin: https://example.com or * (not recommended for secure APIs).
  • For requests with custom headers or non-simple HTTP verbs (PUT/DELETE), browsers send a preflight OPTIONS request to check allowed methods/headers; ensure your server handles OPTIONS properly.

Example (CORS headers returned on server):

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

4) Handle errors & retry patterns

  • Show user-friendly errors for 4xx and 5xx codes.
  • Implement retry strategies for transient errors (e.g., 429 or 5xx), using exponential backoff.

5) Observability & Request IDs

For complex apps, include a X-Request-ID header in client requests that servers log. This request id helps trace errors across services:

const id = crypto.randomUUID();
fetch('/api/data', { headers: { 'X-Request-ID': id } });

On the server, log the X-Request-ID with the request; return it in error responses for a debugging clue.

Example: retry with exponential backoff (simplified)

async function fetchWithRetry(url, options = {}, retries = 3) {
  let attempt = 0;
  while (attempt < retries) {
    try {
      const res = await fetch(url, options);
      if (!res.ok) throw res;
      return await res.json();
    } catch (err) {
      attempt++;
      if (attempt >= retries) throw err;
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 100));
    }
  }
}

5) Use pagination for large datasets

  • APIs often return paginated results: check the API docs for query parameters like ?page=2 or ?limit=20.
  • Support both offset-based pagination (?limit=20&offset=40) and cursor-based pagination (e.g., ?cursor=abc123).

Example: cursor-style pagination (client side)

async function fetchNext(cursor) {
  const url = `/api/items${cursor ? `?cursor=${cursor}` : ''}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error('Failed to fetch page');
  const { data, nextCursor } = await res.json();
  return { data, nextCursor };
}

// Use in loop
let cursor = null;
do {
  const { data, nextCursor } = await fetchNext(cursor);
  render(data);
  cursor = nextCursor;
} while (cursor);

6) Caching & performance

  • Use HTTP cache headers (Cache-Control, ETag) for GET responses.
  • Cache on the client side (memory or local storage) where it makes sense to reduce network requests.
  • Use conditional requests (If-None-Match/ETag) to validate cached responses.

Example: using ETags with conditional requests

// When fetching, store the ETag for later use
const res = await fetch('/api/resource');
const etag = res.headers.get('ETag');
const data = await res.json();
cache.set('resource', { etag, data });

// Later, fetch with If-None-Match
const stored = cache.get('resource');
const res2 = await fetch('/api/resource', { headers: { 'If-None-Match': stored.etag } });
if (res2.status === 304) { return stored.data; }
return res2.json();

7) Security best practices

  • Never store secrets (like API keys) in client-side code.
  • Use HTTPS for all requests.
  • Use token-based auth (JWT or OAuth) and refresh tokens securely.
  • Sanitize and validate user input on the server—not only on the client.

Token storage: localStorage vs HttpOnly cookies

  • localStorage is simple but vulnerable to XSS; if your app runs untrusted third-party scripts, an attacker can read tokens.
  • HttpOnly cookies (with Secure and SameSite attributes) protect tokens from JavaScript access and are recommended for sensitive operations.
  • If you must use localStorage, add strong Content Security Policy (CSP) and sanitize all HTML/inputs to reduce XSS risk.

CSRF

  • When using cookies, protect endpoints using CSRF tokens, same-site cookies or double submit cookies.

OAuth & PKCE for SPAs

  • Use OAuth2 Authorization Code flow with PKCE (Proof Key for Code Exchange) for secure authorization in public clients (browsers).

8) UX patterns (loading states, optimistic updates)

  • Show a spinner or skeleton while fetching data.
  • Use optimistic updates on create/delete to make the UI feel faster; rollback if the server rejects the request.

Example: optimistic update for a TODO

// UI removes item immediately
const removed = todos.splice(index, 1);
render(todos);

// Attempt server delete
try {
  await fetch(`/api/todos/${id}`, { method: 'DELETE' });
} catch (err) {
  // rollback on error
  todos.splice(index, 0, removed[0]);
  render(todos);
}

11) Deployment & runtime architecture (text graph)

Simple example of a typical web app architecture:

Frontend (browser/app) -> CDN -> Load Balancer -> App Server (API) -> Cache (Redis) -> Database (Postgres)

Notes:

  • CDN helps reduce latency by caching static assets (JS, CSS), and edge functions can run lightweight logic near users.
  • Load balancers distribute requests to multiple app server instances for horizontal scaling.
  • Caching layers (like Redis or Varnish) reduce DB load and improve latency for frequent API responses.

12) Handling rate limits & Retry-After

If APIs are rate-limited, servers may return 429 Too Many Requests with a Retry-After header indicating when to retry. Respect it:

async function requestRespectRetry(url, options = {}) {
  const res = await fetch(url, options);
  if (res.status === 429) {
    const retryAfter = res.headers.get('Retry-After');
    const delaySec = retryAfter ? Number(retryAfter) : 2;
    await new Promise(r => setTimeout(r, delaySec * 1000));
    return requestRespectRetry(url, options);
  }
  if (!res.ok) throw res;
  return res.json();
}

9) Use API client libraries when helpful

  • axios is a popular promise-based HTTP client with helpful APIs.
  • For React apps, consider react-query / tanstack/query for caching and server-state management.

Other helpers and tools:

  • SWR — Hooks for data fetching with caching and revalidation.
  • fetch-retry libraries — lightweight wrappers for retries and exponential backoff.
  • OpenAPI / Swagger — document APIs to auto-generate clients and validate contracts.

10) Read the docs & test with tools

  • Use Postman, Insomnia or curl for interactive API testing.
  • Read the API’s docs (OpenAPI/Swagger) to learn required headers and payloads.

Quick Reference (Cheat Sheet)

Methods:

  • GET — Read
  • POST — Create
  • PUT — Replace
  • PATCH — Modify
  • DELETE — Remove

Common status codes:

  • 200 OK — Success
  • 201 Created — Resource created
  • 204 No Content — Success, no content to return
  • 400 Bad Request — Client mistake
  • 401 Unauthorized — Not authenticated
  • 403 Forbidden — Authenticated but not allowed
  • 404 Not Found — Resource doesn’t exist
  • 429 Too Many Requests — Rate limited
  • 500 Internal Server Error — Server error

Conclusion — Start building and learning by doing

HTTP and REST are essential for modern web development. Focus on learning:

  • Which HTTP verb maps to an operation
  • Which status codes signal success and failure
  • Core REST concepts like statelessness and resource-oriented design
  • Practical API consumption techniques: correct headers, error handling, retries, caching, and security

Start by making simple GET and POST calls using fetch or Postman, observe status codes, and handle errors gracefully. Over time, you’ll learn how APIs are designed, how to secure them, and how to build performant client interactions. Happy building! 🚀


Try this (Practice exercise)

Build a small TODO app that consumes JSONPlaceholder:

  1. Create a page that lists TODOs from /todos using fetch.
  2. Add a form to create a new TODO (POST) and render the result locally.
  3. Allow deleting and updating (PATCH) TODOs with optimistic UI updates.
  4. Add loading states, error handling, and show retry logic with an exponential backoff for network errors.

This exercise will help you practice core concepts: GET, POST, PATCH, DELETE, status code handling, and error & retry behavior.

Further reading & resources


Common Pitfalls & Best Practices ⚠️

  • Misusing HTTP verbs: Using POST for what should be a GET (read-only) or using GET to change server state. Follow CRUD mapping to avoid confusion.
  • Forgetting to validate input server-side: Never trust client data—validate and sanitize on the server.
  • Not securing tokens correctly: Avoid storing tokens in localStorage for sensitive applications (XSS risk). Prefer secure, HttpOnly cookies for session tokens; use same-site and secure flags.
  • Retrying non-idempotent requests: Automatically retrying POST requests can create duplicate resources. Only retry safe or idempotent requests or use guarantees (client-generated IDs, dedup tokens).
  • Ignoring CORS & preflight: Understand how preflight OPTIONS requests work and why the server must explicitly allow cross-origin headers/methods.
  • Overusing caches leading to stale data: Ensure caching policies & ETag/Last-Modified headers are set appropriately.
  • Not versioning your API: Use versioning (/api/v1/) to avoid braking clients when introducing incompatible changes.

Pros & Cons: REST vs GraphQL vs gRPC

REST

  • Pros: Simple, HTTP-native, works well with cache/CDN, easy to reason about resources and endpoints.
  • Cons: Over/under-fetching can happen (client may need multiple requests to aggregate data), and designing consistent endpoints requires discipline.

GraphQL

  • Pros: Clients can request exactly the data they need; single endpoint for many resources; great for flexible UIs.
  • Cons: Complexity on server; caching is less straightforward; more work to secure and monitor.

gRPC

  • Pros: Fast, efficient, good for internal microservice communication; strongly typed contracts via Protobuf.
  • Cons: Not native to browsers (requires gRPC-Web proxies), steeper learning curve, tooling trade-offs for public-facing HTTP APIs.

Choose the right tool for the job: REST for standard CRUD-oriented services with cache/CDN benefits; GraphQL for flexible client-driven queries; gRPC for high-performance internal services.

Additional tools & resources

Final tips — keep learning

  • Inspect real API responses with browser DevTools (Network tab) and pay attention to headers.
  • Build a small app (todo list) and a simple express or FastAPI backend to experience both sides.
  • Consider OpenAPI/Swagger for documented APIs and use codegen to generate client libs when applicable.

Comments