# How Plane Abstracts Its API Client Services with APIService

> Discover how Plane abstracts API client services using APIService. Learn to extend this base class for domain-specific endpoints and consistent request configuration.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: architecture
- Published: 2026-08-23

---

**Plane uses a centralized `APIService` base class that wraps Axios and provides thin HTTP method wrappers (`get`, `post`, `put`, `patch`, `delete`), which concrete service classes extend to add domain-specific endpoints while inheriting consistent request configuration.**

All HTTP communication in Plane flows through this abstraction layer, ensuring that authentication credentials, base URLs, and error handling remain uniform across every API call. This design eliminates duplicated networking code and makes the frontend services both testable and type-safe.

## The Core Abstraction: APIService Base Class

The foundation of Plane's API client architecture lives in [`packages/services/src/api.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/api.service.ts). The `APIService` class is declared **abstract**, meaning it cannot be instantiated directly—only extended.

```typescript
// packages/services/src/api.service.ts
export abstract class APIService {
  protected baseURL: string;
  private axiosInstance: AxiosInstance;

  constructor(baseURL: string) {
    this.baseURL = baseURL;
    this.axiosInstance = create({ baseURL, withCredentials: true });
  }

  get(url: string, params = {}, config: AxiosRequestConfig = {}) {
    return this.axiosInstance.get(url, { ...params, ...config });
  }

  post(url: string, data = {}, config: AxiosRequestConfig = {}) {
    return this.axiosInstance.post(url, data, config);
  }

  put(url: string, data = {}, config: AxiosRequestConfig = {}) {
    return this.axiosInstance.put(url, data, config);
  }

  patch(url: string, data = {}, config: AxiosRequestConfig = {}) {
    return this.axiosInstance.patch(url, data, config);
  }

  delete(url: string, config: AxiosRequestConfig = {}) {
    return this.axiosInstance.delete(url, config);
  }

  request<T>(config: AxiosRequestConfig) {
    return this.axiosInstance.request<T>(config);
  }
}

```

Key configuration details from the source:

- **`withCredentials: true`** — ensures cookies and authentication headers are sent with every request
- **`baseURL`** — injected at construction, defaulting to `API_BASE_URL` from environment constants
- **Method signatures** — accept optional `AxiosRequestConfig` for per-request overrides (headers, timeouts, etc.)

## How Concrete Services Extend APIService

Every domain-specific service in Plane inherits from `APIService`. This pattern appears consistently across the codebase, as seen in [`packages/services/src/workspace/workspace.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/workspace/workspace.service.ts):

```typescript
// packages/services/src/workspace/workspace.service.ts
export class WorkspaceService extends APIService {
  constructor(BASE_URL?: string) {
    super(BASE_URL || API_BASE_URL);
  }

  async list(): Promise<IWorkspace[]> {
    return this.get("/api/users/me/workspaces/")
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }

  async create(data: Partial<IWorkspace>): Promise<IWorkspace> {
    return this.post("/api/workspaces/", data)
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }

  async get(workspaceSlug: string): Promise<IWorkspace> {
    return this.get(`/api/workspaces/${workspaceSlug}/`)
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }

  async update(workspaceSlug: string, data: Partial<IWorkspace>): Promise<IWorkspace> {
    return this.patch(`/api/workspaces/${workspaceSlug}/`, data)
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }

  async delete(workspaceSlug: string): Promise<void> {
    return this.delete(`/api/workspaces/${workspaceSlug}/`)
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }
}

```

Notice how `WorkspaceService`:

- Inherits all HTTP machinery from `APIService`
- Only specifies **endpoint paths** and **payload shapes**
- Returns **typed promises** (`Promise<IWorkspace[]>`) using interfaces from `@plane/types`
- Handles errors uniformly by extracting `err?.response?.data`

## Environment-Driven Configuration

The base URL configuration is centralized in [`packages/constants/src/endpoints.ts`](https://github.com/makeplane/plane/blob/main/packages/constants/src/endpoints.ts):

```typescript
// packages/constants/src/endpoints.ts
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";

```

This single constant drives all service instances. When a service is constructed without an explicit base URL, it falls back to this environment-derived value:

```typescript
const workspaceService = new WorkspaceService();        // uses VITE_API_BASE_URL
const customService = new WorkspaceService("https://api.example.com"); // override

```

## Creating a New Service: The Extension Pattern

Adding a new API domain requires minimal boilerplate. Here's how `ProjectService` would follow the established convention:

```typescript
// packages/services/src/project/project.service.ts
import { APIService } from "../api.service";
import { API_BASE_URL } from "@plane/constants";
import type { IProject } from "@plane/types";

export class ProjectService extends APIService {
  constructor(BASE_URL?: string) {
    super(BASE_URL || API_BASE_URL);
  }

  async get(projectId: string): Promise<IProject> {
    return this.get(`/api/projects/${projectId}/`)
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }

  async list(workspaceSlug: string): Promise<IProject[]> {
    return this.get(`/api/workspaces/${workspaceSlug}/projects/`)
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }

  async create(workspaceSlug: string, data: Partial<IProject>): Promise<IProject> {
    return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
      .then(res => res?.data)
      .catch(err => { throw err?.response?.data; });
  }
}

```

## Benefits of Plane's API Client Abstraction

| Benefit | How APIService Delivers |
|--------|------------------------|
| **Consistency** | All services share identical Axios configuration (credentials, headers, interceptors) |
| **DRY Code** | HTTP verb implementations exist once; services only define paths and types |
| **Type Safety** | Generic methods return typed responses via `@plane/types` interfaces |
| **Testability** | The base class can be mocked; unit tests verify service logic without network calls |
| **Environment Flexibility** | Switching API environments only changes `VITE_API_BASE_URL` |

## Using Services in Application Code

```typescript
import { WorkspaceService, UserService } from "@plane/services";

// Services are typically instantiated once per module or passed via context
const wsService = new WorkspaceService();
const userService = new UserService();

// Typed responses with full autocomplete
const workspaces: IWorkspace[] = await wsService.list();
const currentUser: IUser = await userService.getCurrentUser();

```

## Summary

- **APIService** in [`packages/services/src/api.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/api.service.ts) is the single abstraction point for all HTTP communication in Plane.

- **Concrete services** (e.g., `WorkspaceService`, `UserService`) extend this base class and only implement domain-specific endpoint paths.

- **Configuration** flows from environment variables through `API_BASE_URL` in [`packages/constants/src/endpoints.ts`](https://github.com/makeplane/plane/blob/main/packages/constants/src/endpoints.ts), ensuring environment-agnostic service code.

- **Type safety** is enforced by returning typed promises using interfaces from the `@plane/types` package.

- **Error handling** is standardized: services extract `response.data` on success and `response.data` from Axios errors for consistent failure reporting.

## Frequently Asked Questions

### What HTTP library does Plane use under APIService?

Plane uses **Axios** as the underlying HTTP client. The `APIService` constructor creates a private `AxiosInstance` via the `create()` method from `axios`, configured with `withCredentials: true` for automatic cookie handling and the injected `baseURL`.

### Can I override the base URL for a specific service instance?

Yes. Every concrete service accepts an optional `BASE_URL` constructor parameter that overrides the default `API_BASE_URL`. This enables pointing individual services to different endpoints without modifying the base class or environment variables.

### How does Plane handle API errors consistently?

Services follow a uniform pattern: they chain `.then(res => res?.data)` to extract successful response bodies, and `.catch(err => { throw err?.response?.data })` to bubble API error payloads. This ensures callers receive predictable error shapes regardless of which service method failed.

### Why is APIService declared as an abstract class?

The `abstract` modifier prevents direct instantiation of `APIService`, enforcing that all API clients must extend it with domain-specific methods. This design choice guides developers toward the established pattern and ensures no unconfigured HTTP client exists in the codebase.