Back to Blog
Lesson 29 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 16, 20263 min read

Building the Task API Client: Setup and Architecture

Learn how to build a production-ready API client in TypeScript. We’ll establish the foundational architecture, define core interfaces, and setup fetch logic.

TypeScriptAPIFrontendArchitectureFetchTasks
Creative young man working on a strategy plan on a whiteboard at the office.

Previously in this course, we covered everything from type assertions to non-null assertions. Now that you’ve mastered the foundational building blocks, it’s time to apply them to a real-world scenario. In this lesson, we will initialize the structure for a modular API client, ensuring our frontend communicates with our Task Management API with full type safety.

The Architecture of a Robust API Client

When building an API client, avoid putting fetch calls directly into your UI components. Instead, we want a dedicated layer that acts as a bridge between the network and our application logic. This separation is crucial for maintaining a clean client-server architecture, as it allows us to swap out endpoints or add global headers without touching our components.

Our client will follow a modular pattern:

  1. Definitions: Where our data models (interfaces) live.
  2. Configuration: Constants for base URLs and headers.
  3. Client: The functions that perform the actual requests.

Defining the Base Task Interface

Before we write a single line of fetch logic, we need to define the "shape" of our data. Since we’ve already explored defining object shapes with interfaces, we can confidently define our Task model.

Create a file named src/types/task.ts:

TYPESCRIPT
export interface Task {
  id: string;
  title: string;
  description: string;
  status: CE9178">'pending' | CE9178">'in-progress' | CE9178">'completed';
  dueDate: string;
}

By centralizing this interface, any changes to the API response structure can be updated in one place, and TypeScript will immediately alert us to any breaking changes elsewhere in our app.

Setting Up the API Client Structure

We’ll create a dedicated TaskClient to encapsulate our logic. This module will handle the heavy lifting, keeping our components lean.

Create src/api/taskClient.ts:

TYPESCRIPT
import { Task } from CE9178">'../types/task';

const BASE_URL = CE9178">'https://api.myapp.com/v1';

export const taskClient = {
  async fetchTasks(): Promise<Task[]> {
    const response = await fetch(CE9178">`${BASE_URL}/tasks`);
    
    if (!response.ok) {
      throw new Error(CE9178">'Failed to fetch tasks');
    }

    return response.json();
  }
};

Why This Setup Matters

By defining fetchTasks as an async function that returns a Promise<Task[]>, we leverage TypeScript’s ability to track asynchronous data flow. If we later decide to add query logic or implement versioned routes, we only need to modify this central client.

Hands-on Exercise

To solidify this, I want you to expand the client.

  1. Create a TaskInput type alias (using the lessons we learned on type aliases) that excludes the id field from our Task interface.
  2. Add a createTask method to the taskClient object that takes a TaskInput and returns the newly created Task.

Common Pitfalls

  • Hardcoding URLs everywhere: Always define your BASE_URL in one place. If you hardcode it in every function, you'll spend hours updating it when you move from development to production.
  • Ignoring Error States: Fetch doesn't throw on 404s or 500s. Always check response.ok before attempting to parse the JSON.
  • Over-complicating early: Don't try to build a complex generic fetch wrapper yet. Keep it simple; we will introduce advanced generic request wrappers in later lessons.

FAQ

Q: Should I use interface or type for the Task model? A: For data models that might be extended or implemented by classes, interface is often preferred. For simple data structures, they are largely interchangeable.

Q: Why not use Axios? A: The native fetch API is sufficient for most modern applications. We are focusing on fetch to keep our dependencies low and our understanding of the underlying browser APIs high.

Recap

We’ve successfully initialized our API client structure by defining a core Task interface and establishing a modular taskClient. This provides the foundation for all future API interactions, ensuring we can handle POST requests and data fetching with confidence.

Up next: We will dive into Typing API Responses to ensure the data coming back from our server matches our internal expectations.

Similar Posts