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.

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 generic wrapper should accept a URL and optional configuration, returning a Promise of the expected type T.
TYPESCRIPTasync 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
- Generic Type Parameter
<T>: This acts as a placeholder for the data type we expect to receive (e.g.,TaskorTask[]). RequestInit: This is a built-in TypeScript type forfetchoptions, allowing us to pass headers, methods, or bodies easily.response.okCheck: We catch non-2xx status codes immediately. Without this,fetchwon't throw an error on a 404 or 500, which leads to silent failures.- Type Assertion: By using
as Promise<T>, we tell TypeScript to treat the output ofresponse.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.
TYPESCRIPTinterface 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
- Create a file named
api.ts. - Copy the
apiClientfunction provided above. - Define an interface
Taskwith propertiesid,title, andcompleted. - Write a function
postTask(task: Omit<Task, 'id'>)that usesapiClientto send a POST request with the new task data. - Hint: You will need to pass
{ method: 'POST', body: JSON.stringify(task) }as the second argument toapiClient.
Common Pitfalls
- Forgetting
awaitonresponse.json(): Thejson()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 Tis 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

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.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.


