Frontend Development

Chapter 10
APIs and Asynchronous Integration

Certificate in Full stack programming
Frontend Development   •   Certificate

APIs and Asynchronous Integration

Introduction

Modern frontend applications rarely operate in isolation. A weather dashboard, online store, banking portal, and learning platform all obtain data from remote services. These services expose application programming interfaces (APIs) that allow software systems to communicate through defined requests and responses.

Frontend API integration requires more than writing a request. A reliable application must understand HTTP, JSON, promises, asynchronous control flow, authentication, loading states, errors, pagination, caching, CORS, and API contracts. It must also protect user data and avoid exposing credentials or trusting unvalidated responses.

This chapter focuses on practical REST and GraphQL integration using JavaScript and browser APIs.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain how frontend applications communicate with APIs;
  • distinguish REST and GraphQL approaches;
  • use fetch, promises, and async/await;
  • parse, validate, and transform JSON responses;
  • implement loading, success, empty, and error states;
  • describe authentication methods and frontend security limits;
  • implement basic pagination and caching;
  • explain CORS and preflight requests;
  • interpret API contracts and design maintainable integration code.

1. APIs as Contracts Between Systems

An API is a published interface through which one program requests capabilities or data from another. In a web application, the browser is commonly the client, while a remote server provides resources through an HTTP API.

A request generally contains:

  • an HTTP method, such as GET or POST;
  • a URL, often called an endpoint;
  • headers, such as Accept and Authorization;
  • optional query parameters;
  • an optional request body.

A response generally contains:

  • a status code;
  • response headers;
  • an optional body, commonly JSON;
  • timing and network information available to developer tools.

HTTP Methods and Status Codes

MethodTypical purposeUsually has a request body?
GETRetrieve dataNo
POSTCreate a resource or perform an actionYes
PUTReplace a resourceYes
PATCHPartially update a resourceYes
DELETERemove a resourceUsually no

Status codes communicate broad outcomes. 200 OK indicates a successful request, 201 Created indicates successful creation, and 204 No Content indicates success without a response body. 400 Bad Request represents invalid input, 401 Unauthorized usually means missing or invalid authentication, 403 Forbidden means the server understood the request but refuses it, and 404 Not Found means the resource is unavailable at that location. 429 Too Many Requests indicates rate limiting, while 500-series codes indicate server-side failures.

A frontend should not treat every response as successful merely because a network connection was made. The browser's fetch function rejects its promise mainly for network-level failures; HTTP errors such as 404 and 500 normally still produce a fulfilled response. Therefore, application code must explicitly inspect response.ok or response.status.

2. REST and GraphQL

REST

REST, or Representational State Transfer, organizes functionality around resources. A product service might expose:

  • GET /api/products to list products;
  • GET /api/products/42 to retrieve product 42;
  • POST /api/products to create a product;
  • PATCH /api/products/42 to modify product 42.

REST commonly uses standard HTTP methods and status codes. It is easy to inspect with browser tools and command-line utilities. However, a client may receive too much data or need several requests to construct one screen.

GraphQL

GraphQL normally exposes one endpoint, often /graphql. The client sends a query describing the fields it needs:

const query = `
  query ProductDetails($id: ID!) {
    product(id: $id) {
      id
      name
      price
      reviews { rating }
    }
  }
`;

const response = await fetch('/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query, variables: { id: '42' } })
});

GraphQL can reduce over-fetching and combine related data. Its response may contain both data and errors, so checking only the HTTP status is insufficient. GraphQL also requires careful query design, authorization rules, and protection against expensive or excessively nested queries.

REST and GraphQL are not automatically superior in every situation. REST often fits resource-oriented operations and HTTP caching naturally. GraphQL is useful when clients need flexible, connected data. The correct choice depends on the service contract, team capability, performance requirements, and security model.

3. JSON and Data Transformation

JSON (JavaScript Object Notation) represents objects, arrays, strings, numbers, booleans, and null. A JSON object is not automatically a trusted domain model. It is external input and must be checked before the interface relies on it.

{
  "id": 42,
  "name": "Keyboard",
  "price": 29.99,
  "available": true,
  "tags": ["input", "usb"]
}

The browser converts a JSON response using response.json():

const response = await fetch('/api/products/42');
if (!response.ok) {
  throw new Error(`Request failed: ${response.status}`);
}

const product = await response.json();
console.log(product.name);

Parsing can fail if the body is not valid JSON. A robust integration also checks expected fields:

function isProduct(value) {
  return value !== null &&
    typeof value === 'object' &&
    typeof value.id === 'number' &&
    typeof value.name === 'string' &&
    typeof value.price === 'number';
}

For larger applications, schema-validation libraries can enforce contracts at runtime. Whether validation is manual or library-based, the principle is the same: external data should be converted into a form the application understands.

Transformation separates server representation from view representation:

function toProductView(product) {
  return {
    id: product.id,
    title: product.name.trim(),
    displayPrice: new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(product.price)
  };
}

This prevents formatting concerns from spreading through the application.

4. Promises and Asynchronous Control Flow

A network request takes time. JavaScript therefore represents its future result with a promise. A promise may be pending, fulfilled, or rejected.

fetch('/api/products')
  .then(response => {
    if (!response.ok) throw new Error('Unable to load products');
    return response.json();
  })
  .then(products => renderProducts(products))
  .catch(error => showError(error));

async/await provides a sequential style for promise-based code:

async function loadProducts() {
  const response = await fetch('/api/products');
  if (!response.ok) {
    throw new Error(`HTTP error ${response.status}`);
  }
  return response.json();
}

async function startPage() {
  try {
    const products = await loadProducts();
    renderProducts(products);
  } catch (error) {
    showError(error);
  }
}

An async function always returns a promise. await pauses that function, not the entire browser. The interface can continue responding to other events.

Independent requests should usually run concurrently:

const [profile, notifications] = await Promise.all([
  fetch('/api/profile').then(checkJson),
  fetch('/api/notifications').then(checkJson)
]);

If any promise in Promise.all rejects, the combined promise rejects. When partial success is acceptable, Promise.allSettled allows each outcome to be inspected.

Sequential requests are appropriate when the second depends on the first, such as obtaining an order identifier before requesting its details. Unnecessary sequential requests increase waiting time.

5. Loading, Empty, Success, and Error States

A professional interface models the full request lifecycle:

  1. Idle: no request has started.
  2. Loading: a request is in progress.
  3. Success: valid data is available.
  4. Empty: the request succeeded but returned no useful items.
  5. Error: the request failed or the response was invalid.

The loading indicator should be shown before starting the request and removed in a finally block:

async function refreshProducts() {
  setState({ status: 'loading' });

  try {
    const response = await fetch('/api/products');
    if (!response.ok) throw new Error(`Status ${response.status}`);
    const data = await response.json();

    if (!Array.isArray(data)) throw new Error('Invalid product list');
    setState({ status: data.length ? 'success' : 'empty', data });
  } catch (error) {
    setState({ status: 'error', message: 'Products could not be loaded.' });
    logError(error);
  } finally {
    hideNetworkSpinner();
  }
}

Users should receive useful, non-technical messages. Developers should receive diagnostic information through secure logs. Do not display access tokens, stack traces, or sensitive server details in the interface.

Cancellation and Stale Responses

A user may change a search term while an earlier request is still running. The older response could arrive last and overwrite newer results. AbortController can cancel obsolete requests:

let controller;

async function searchProducts(term) {
  controller?.abort();
  controller = new AbortController();

  const response = await fetch(`/api/products?q=${encodeURIComponent(term)}`, {
    signal: controller.signal
  });
  if (!response.ok) throw new Error('Search failed');
  return response.json();
}

An aborted request should normally be distinguished from a server failure so that the user is not shown an unnecessary error.

6. Authentication and Security

Common authentication approaches include:

  • API keys: identify an application or consumer. They are not secret when placed in browser code.
  • Session cookies: the browser stores a cookie and sends it to the appropriate origin. Secure systems use HttpOnly, Secure, and suitable SameSite settings.
  • Bearer tokens: a request includes Authorization: Bearer <token>.
  • OAuth 2.0 and OpenID Connect: delegated authorization and identity protocols commonly used with external identity providers.

A frontend cannot keep a long-term secret truly secret. Anything shipped to the browser can potentially be inspected. Sensitive API keys should therefore be kept on a trusted backend or restricted by origin, scope, quota, and allowed operations.

For cookie-based authentication, cross-site request forgery (CSRF) must be considered. Defenses include same-site cookies, CSRF tokens, origin checking, and server-side validation. For token-based applications, storage decisions matter: browser storage can be exposed by cross-site scripting (XSS). Strong output encoding, content security policy, dependency review, and avoidance of unsafe HTML insertion reduce risk.

Authentication answers “Who is making the request?” Authorization answers “What may that identity do?” The server must enforce authorization; hiding a button in the frontend is not a security control.

7. CORS and the Same-Origin Policy

The browser's same-origin policy restricts scripts from freely reading responses from another origin. An origin is defined by scheme, host, and port. For example, https://app.example.com and https://api.example.com have different origins because their hosts differ.

Cross-Origin Resource Sharing (CORS) is an HTTP mechanism through which a server states which cross-origin browser requests may be read. A server may return:

Access-Control-Allow-Origin: https://app.example.com

A simple cross-origin request may be sent directly. A request with non-simple characteristics—such as certain methods or custom headers—can cause the browser to send an OPTIONS preflight request first. The server must approve the intended method and headers, for example with Access-Control-Allow-Methods and Access-Control-Allow-Headers.

CORS is enforced by browsers, not by the API's general network accessibility. A request may work in a server-side script or API client but fail in browser JavaScript because the server did not grant permission. Frontend code cannot legitimately bypass CORS with a browser setting. A controlled backend proxy, correctly configured server headers, or same-origin deployment is the appropriate solution.

Credentialed requests require special care. A server cannot use a wildcard origin (*) together with credentials. CORS configuration should allow only necessary origins, methods, and headers.

8. API Contracts and Versioning

An API contract describes the expected requests and responses. It may be documented with OpenAPI for REST or a schema for GraphQL. A useful contract specifies:

  • endpoint and method;
  • parameters and their types;
  • required headers;
  • request and response schemas;
  • status codes and error format;
  • authentication requirements;
  • pagination behavior;
  • rate limits and version policy.

Contracts reduce assumptions between frontend and backend teams. A breaking change might rename a field, change a type, remove an endpoint, or alter authentication behavior. Adding an optional response field is usually less disruptive than changing an existing field's meaning.

Versioning strategies include URL versions such as /v2/products, request headers, and backward-compatible evolution. Frontend teams should use contract tests, mock responses, and automated integration tests rather than discovering changes only after deployment.

9. Pagination

Large collections should not normally be downloaded in one response. Pagination divides results into manageable portions.

Offset pagination uses parameters such as page=3&limit=20. It is simple and supports direct page navigation, but changing data between requests can cause duplicates or omissions.

Cursor pagination uses a server-provided cursor, such as nextCursor. It is often more stable for changing datasets, especially when the server orders records consistently, but cursors are opaque and usually do not support arbitrary page jumps.

async function loadPage(cursor) {
  const url = new URL('/api/products', window.location.origin);
  url.searchParams.set('limit', '20');
  if (cursor) url.searchParams.set('cursor', cursor);

  const response = await fetch(url);
  if (!response.ok) throw new Error('Page request failed');
  return response.json(); // { items: [...], nextCursor: '...' }
}

The interface should disable “Next” when no cursor exists, prevent duplicate submissions, and preserve the sorting rule used by the API. Accessibility matters: loading new results should be announced appropriately, and focus should not be lost unexpectedly.

10. Caching and Freshness

Caching avoids repeated work and improves perceived performance. Possible layers include the browser's HTTP cache, a service worker cache, an application memory cache, and a server-side cache.

A simple in-memory cache can store a value and its timestamp:

const cache = new Map();
const maxAge = 60_000; // milliseconds

async function getCached(url) {
  const now = Date.now();
  const item = cache.get(url);
  if (item && now - item.time < maxAge) return item.value;

  const response = await fetch(url);
  if (!response.ok) throw new Error('Request failed');
  const value = await response.json();
  cache.set(url, { value, time: now });
  return value;
}

The freshness condition is now − time < maxAge, where all three quantities are measured in milliseconds. This is a simple time-to-live policy, not a universal caching rule. It is appropriate only when slightly stale data is acceptable and the cache key includes every input that affects the response.

HTTP caching is often preferable because the server can provide Cache-Control, ETag, and Last-Modified metadata. A cached response must not leak one user's private data to another user. Mutation operations should invalidate or update affected cache entries. More advanced patterns include stale-while-revalidate, in which cached data is displayed immediately while a background request checks for a newer version.

Worked Example: A Searchable Product List

Suppose an online shop needs a search box and paginated results. The endpoint returns:

{
  "items": [{ "id": 7, "name": "USB Keyboard", "price": 29.99 }],
  "nextCursor": "abc123"
}

A suitable design is:

  1. debounce typing so every keystroke does not create a request;
  2. cancel the previous request when a new search begins;
  3. encode the search term as a query parameter;
  4. display a loading state while the first page is requested;
  5. validate that items is an array;
  6. render an empty state when no items are returned;
  7. retain nextCursor for the next-page action;
  8. show a retry action for recoverable failures;
  9. avoid caching private or rapidly changing results without a clear policy.

The important design insight is that request code and presentation state are related but distinct. A reusable service function can retrieve and validate data, while a component decides how loading, results, and errors appear.

Applied Case: Course Dashboard Integration

This is an illustrative case. A college course dashboard displays a student's enrolled modules, upcoming assessments, and notification count. The frontend must combine three services.

Background and Problem

The initial implementation waits for modules, then assessments, then notifications. Students see a blank page for several seconds. Errors are displayed as raw server messages, and a token has accidentally been committed to a public repository.

Analysis

The three requests are independent and can run concurrently. The token is not a safe browser secret. The interface also lacks distinct empty and error states, and the API response is used without validating its shape.

Approach and Solution

The team moves authentication to a backend session using secure cookies. The frontend requests the three resources through a same-origin gateway. It uses Promise.allSettled because the dashboard can still show modules if notifications fail. Each response is checked for an acceptable status and validated before rendering. A shared state model represents loading, partial success, empty data, and failure. A short-lived cache is used for the notification count, while assessment data is revalidated after a mutation.

Outcome

The dashboard becomes usable as soon as available sections are ready. Users receive clear messages, credentials are removed from client code, and contract tests detect an accidental change from assessmentDate to date before release.

Learning Points

The case demonstrates that API integration is an architectural concern, not merely a fetch call. Concurrency, security, contracts, resilience, and user experience must be designed together.

Challenges and Limitations

API integration remains difficult because networks are unreliable, services evolve, and latency is variable. A request may time out, succeed on the server but fail during response transfer, or return valid JSON with unexpected meaning. Retries can help transient failures but may duplicate non-idempotent operations such as payments. Retry policies should use limits, delay, and server guidance such as Retry-After.

Rate limits require responsible client behavior: debounce searches, paginate, cache suitable data, and avoid polling unnecessarily. Observability should record request identifiers, latency, status categories, and failure rates without recording sensitive payloads.

Accessibility must be considered in asynchronous interfaces. Loading indicators should communicate status without causing excessive visual or screen-reader noise. Error messages should be understandable, actionable, and associated with the relevant control.

AI-assisted coding can generate request wrappers quickly, but generated code may mishandle authentication, ignore response.ok, expose secrets, or trust unvalidated JSON. Developers remain responsible for reviewing security, contracts, error paths, and license or privacy implications.

Best Practices Checklist

  • Treat API responses as untrusted external input.
  • Check HTTP status explicitly and handle JSON parsing failures.
  • Use async/await with try/catch/finally for readable control flow.
  • Run independent requests concurrently when appropriate.
  • Model loading, empty, success, partial, and error states.
  • Cancel obsolete requests and prevent stale results from replacing current data.
  • Never place unrestricted secrets in frontend code.
  • Configure CORS on the server with the narrowest practical permissions.
  • Document and test API contracts.
  • Use pagination, caching, and rate-aware request patterns.
  • Protect personal data and avoid logging tokens or sensitive payloads.
  • Test slow networks, offline conditions, malformed responses, expired sessions, and authorization failures.

Chapter Summary

APIs provide formal communication channels between frontend applications and remote services. REST organizes operations around resources and HTTP methods, while GraphQL lets clients request a selected data shape through a schema-driven endpoint. JavaScript promises and async/await make asynchronous operations manageable, but developers must explicitly check HTTP status codes because fetch does not reject ordinary HTTP errors.

Reliable applications parse and validate JSON, transform server data into view models, represent complete request states, cancel obsolete requests, and distinguish recoverable failures from invalid input or authorization problems. Authentication and authorization are different responsibilities, and browser-delivered secrets cannot be treated as confidential. CORS governs whether browser scripts may read cross-origin responses; it is solved through server configuration or an appropriate backend architecture, not frontend bypasses.

Pagination controls large collections, while caching reduces latency and repeated work when freshness and privacy rules permit it. API contracts, versioning, testing, observability, accessibility, and security make integration maintainable in professional environments.

Key Terms

API: A defined interface through which software systems communicate.

Endpoint: A network address representing an API operation or resource.

HTTP method: A verb indicating the intended operation, such as GET or POST.

Promise: An object representing the eventual result of an asynchronous operation.

async/await: JavaScript syntax for writing promise-based code in a sequential style.

JSON: A text-based format for structured data exchange.

REST: A resource-oriented architectural style using HTTP conventions.

GraphQL: A query language and runtime in which clients request specified fields.

Authentication: Verification of an identity.

Authorization: Determination of what an authenticated identity may do.

Bearer token: A token presented in an authorization header as proof of access.

CORS: HTTP rules that govern browser access to cross-origin responses.

Preflight: An OPTIONS request used to ask whether a cross-origin request is permitted.

API contract: A documented agreement describing requests, responses, and errors.

Pagination: Dividing a large collection into multiple responses.

Cursor: An opaque server-provided value identifying a position in a paginated collection.

Cache: Stored data used to avoid repeated retrieval or computation.

TTL: Time to live; the period for which cached data is considered fresh.

CSRF: An attack that causes a user's browser to submit an unwanted authenticated request.

XSS: Injection of executable script into a user's browser context.

Review Questions

Short Answer Questions

  1. Why must frontend code check response.ok when using fetch?
  2. What is the difference between authentication and authorization?
  3. State two differences between REST and GraphQL.
  4. What purpose does a CORS preflight request serve?
  5. Why is response validation important when processing JSON?

Long Answer Questions

  1. Explain the lifecycle of an asynchronous API request and describe appropriate user-interface states.
  2. Compare offset pagination and cursor pagination, including one advantage and one limitation of each.
  3. Explain how promises, async/await, Promise.all, and Promise.allSettled support frontend integration.
  4. Discuss the security limitations of API keys and bearer tokens in browser applications.
  5. Describe how API contracts, versioning, and automated tests reduce integration failures.

Analytical / Application Questions

  1. A search interface displays older results after a user enters a new search term. Diagnose the cause and propose a solution using cancellation or request identity.
  2. A request succeeds in an API testing tool but fails in browser JavaScript with a CORS error. Explain why and identify an appropriate architectural remedy.
  3. Design an integration strategy for a dashboard with three independent endpoints, one of which may fail without preventing the other two from appearing. Justify your choice of concurrency method, state model, and error handling.

Multiple Choice Questions with Answers

Question 1

What does fetch normally do when the server returns HTTP 404?

A. It automatically retries the request

B. It rejects the promise solely because of the 404 status

C. It returns a fulfilled Response that must be checked

D. It converts the response into valid JSON automatically

Correct Answer: C. It returns a fulfilled Response that must be checked

Question 2

Which method is generally used to partially update an existing REST resource?

A. GET

B. PATCH

C. OPTIONS

D. HEAD

Correct Answer: B. PATCH

Question 3

Which JavaScript feature is most appropriate for cancelling an obsolete fetch request?

A. AbortController

B. JSON.parse

C. Promise.race only

D. Intl.NumberFormat

Correct Answer: A. AbortController

Question 4

What is a principal advantage of GraphQL queries?

A. They eliminate the need for authorization

B. They allow the client to request specified fields

C. They cannot return errors in the response

D. They require a separate URL for every field

Correct Answer: B. They allow the client to request specified fields

Question 5

What does CORS primarily control in a browser?

A. Whether a script may read a cross-origin response

B. Whether JSON can contain arrays

C. Whether a server can store database records

D. Whether JavaScript promises can be created

Correct Answer: A. Whether a script may read a cross-origin response

Question 6

Which status code commonly indicates that a client has exceeded a rate limit?

A. 201

B. 204

C. 429

D. 301

Correct Answer: C. 429

Question 7

Why should a frontend not treat an API key embedded in its JavaScript bundle as secret?

A. Browsers cannot send API keys

B. Users can inspect code delivered to their browsers

C. API keys work only with GraphQL

D. HTTP does not support authentication headers

Correct Answer: B. Users can inspect code delivered to their browsers

Question 8

Which method is usually best when independent requests should be awaited together and any failure should reject the combined operation?

A. Promise.all

B. Promise.parse

C. Array.filter

D. JSON.stringify

Correct Answer: A. Promise.all

Question 9

In cursor pagination, what does the next cursor usually represent?

A. A password for the next user

B. An opaque position for retrieving the next results

C. The total number of records in the database

D. A replacement for HTTP status codes

Correct Answer: B. An opaque position for retrieving the next results

Question 10

What is the purpose of a cache TTL?

A. To define how long cached data is considered fresh

B. To encrypt every response permanently

C. To increase the number of network requests

D. To authenticate a user without credentials

Correct Answer: A. To define how long cached data is considered fresh

Frontend Development
Chapter 10 of 15