Back to Blog
Lesson 31 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 18, 20264 min read

Creating a Generic Request Wrapper in TypeScript

Master API communication by building a generic fetch wrapper in TypeScript. Learn to centralize request logic, handle JSON parsing, and maintain type safety.

TypeScriptGenericsFetchAPIWrapper
Close-up of a laptop keyboard with a note reading 'Coffee Please!' placed on top, suggesting a coffee break.

Previously in this course, we covered typing API responses to ensure our application knows exactly what data to expect from the server. In this lesson, we’ll move from defining interfaces to building the engine that actually fetches that data: a generic request wrapper.

By the end of this lesson, you will be able to write a single, reusable function that abstracts away the repetitive parts of using fetch, ensuring consistent error handling and type safety across your entire task application.

The Problem: Repetitive Fetch Logic

When working with APIs, you often find yourself repeating the same boilerplate code: calling fetch, checking response.ok, and parsing the JSON body. If you do this in every single component, you end up with a maintenance nightmare.

Standardizing this process using Generics and Fetch allows us to define the "shape" of our expected data once, and let TypeScript do the heavy lifting of verifying that the response matches that shape.

Building a Generic API Wrapper

A ball of twine on a green surface creating a minimalist still life composition perfect for design projects.

A generic wrapper should accept a URL and optional configuration, returning a Promise of the expected type T.

TYPESCRIPT
async function apiClient<T>(url: string, options: RequestInit = {}): Promise<T> {
  const response = await fetch(url, {
    ...options,
    headers: {
      CE9178">'Content-Type': CE9178">'application/json',
      ...options.headers,
    },
  });

  if (!response.ok) {
    throw new Error(CE9178">`API Error: ${response.statusText}`);
  }

  return response.json() as Promise<T>;
}

Breaking Down the Implementation

  1. Generic Type Parameter <T>: This acts as a placeholder for the data type we expect to receive (e.g., Task or Task[]).
  2. RequestInit: This is a built-in TypeScript type for fetch options, allowing us to pass headers, methods, or bodies easily.
  3. response.ok Check: We catch non-2xx status codes immediately. Without this, fetch won't throw an error on a 404 or 500, which leads to silent failures.
  4. Type Assertion: By using as Promise<T>, we tell TypeScript to treat the output of response.json() as the type we requested.

Worked Example: Fetching a Single Task

Let's use our apiClient to fetch a task from our running project, building on the foundations laid in our Building the Task API Client: Setup and Architecture lesson.

TYPESCRIPT
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

async function getTask(id: number): Promise<Task> {
  // TypeScript now knows this returns a Task object!
  return apiClient<Task>(CE9178">`/api/tasks/${id}`);
}

If we change the API structure in the future, we only need to update the apiClient logic in one place rather than hunting through dozens of service files.

Hands-on Exercise

  1. Create a file named api.ts.
  2. Copy the apiClient function provided above.
  3. Define an interface Task with properties id, title, and completed.
  4. Write a function postTask(task: Omit<Task, 'id'>) that uses apiClient to send a POST request with the new task data.
  5. Hint: You will need to pass { method: 'POST', body: JSON.stringify(task) } as the second argument to apiClient.

Common Pitfalls

  • Forgetting await on response.json(): The json() method returns a promise. Always remember to await it, or your wrapper will return a Promise of a Promise, leading to "undefined" data in your components.
  • Assuming Types are Safe: Remember that as T is a type assertion, not a runtime check. It tells TypeScript "trust me, this is what the server sent." In production, consider using a library like Zod to validate the data at runtime.
  • Hardcoding Headers: Always spread the existing headers (...options.headers) if you add your own. If you overwrite the headers object entirely, you might strip away essential tokens or content-type settings provided by the caller.

FAQ

Why not just use fetch directly? Using fetch directly is fine for small scripts, but in an application, you need to handle errors, headers, and parsing consistently. A wrapper prevents bugs caused by forgetting to check response.ok.

Is as T safe? It is "assertion-based" safety. It ensures your code compiles assuming the structure is correct. It does not prevent the server from sending the wrong data. For full safety, validate the payload against your interface at runtime.

Recap

Team members presenting a project in a modern office setting with a focus on collaboration.

We've moved beyond manual fetch calls by creating a centralized, type-safe apiClient. By leveraging Generics, we ensure that our API layer remains flexible enough to handle any data structure while maintaining strict type checking. This standardization is a crucial step in building a robust task manager project.

Up next: We will dive deeper into Handling API Errors to ensure our application gracefully manages failed requests and network issues.

Similar Posts