Como construir um SDK para sua API pública em TypeScript
1. Fundamentos e planejamento do SDK
Antes de escrever uma linha de código, é essencial definir o escopo do SDK. Um SDK bem projetado deve encapsular os endpoints da sua API pública de forma intuitiva, sem vazar detalhes de implementação. A primeira decisão é escolher entre um SDK monolítico (uma única classe com todos os métodos) ou modular (separação por recursos como users, products, orders). Para APIs com mais de 10 endpoints, a abordagem modular é fortemente recomendada.
O versionamento semântico (SemVer) deve espelhar a versão da API. Se sua API está na v2, seu SDK deve começar como 2.0.0. Isso evita confusão quando a API sofre breaking changes.
// Exemplo de estrutura modular de recursos
src/
client/
ApiClient.ts
types.ts
resources/
Users.ts
Products.ts
Orders.ts
index.ts
2. Configuração do projeto TypeScript
A estrutura de diretórios padrão para bibliotecas TypeScript inclui src/ para código fonte, types/ para declarações de tipos compartilhados, tests/ para testes e dist/ para o build final. O tsconfig.json deve ser configurado com strict: true e declaration: true para gerar arquivos .d.ts.
// tsconfig.json básico para SDK
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "tests"]
}
Dependências mínimas: axios para requisições HTTP (ou node-fetch para ambientes sem fetch nativo) e uuid para IDs únicos em operações de batch. Evite dependências pesadas que aumentem o bundle size.
3. Definição de tipos e interfaces
Tipos fortes são a alma de um SDK TypeScript. Cada endpoint da API deve ter interfaces correspondentes para payloads de requisição e resposta. A configuração do cliente também deve ser tipada.
// types/api.ts
export interface ApiConfig {
baseURL: string;
apiKey?: string;
accessToken?: string;
timeout?: number;
headers?: Record<string, string>;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
perPage: number;
hasMore: boolean;
}
export interface ApiError {
status: number;
message: string;
code?: string;
details?: Record<string, string[]>;
}
// types/users.ts
export interface User {
id: string;
name: string;
email: string;
createdAt: string;
}
export interface CreateUserPayload {
name: string;
email: string;
password: string;
}
export interface UpdateUserPayload {
name?: string;
email?: string;
}
4. Cliente HTTP e tratamento de requisições
A classe ApiClient centraliza toda a comunicação HTTP. Métodos genéricos como get<T>(), post<T>(), put<T>() e delete<T>() garantem reutilização com tipagem completa. Interceptors são ideais para injetar tokens de autenticação automaticamente.
// client/ApiClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ApiConfig, ApiError } from '../types/api';
export class ApiClient {
private client: AxiosInstance;
private config: ApiConfig;
constructor(config: ApiConfig) {
this.config = {
timeout: 10000,
...config,
};
this.client = axios.create({
baseURL: this.config.baseURL,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
});
this.setupInterceptors();
}
private setupInterceptors(): void {
this.client.interceptors.request.use((config) => {
if (this.config.apiKey) {
config.headers['X-API-Key'] = this.config.apiKey;
}
if (this.config.accessToken) {
config.headers['Authorization'] = `Bearer ${this.config.accessToken}`;
}
return config;
});
this.client.interceptors.response.use(
(response) => response,
(error) => {
const apiError: ApiError = {
status: error.response?.status || 500,
message: error.response?.data?.message || error.message,
code: error.response?.data?.code,
details: error.response?.data?.details,
};
return Promise.reject(apiError);
}
);
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.get(url, config);
return response.data;
}
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.post(url, data, config);
return response.data;
}
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.put(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.delete(url, config);
return response.data;
}
}
Para retry com backoff exponencial, utilize uma biblioteca como axios-retry ou implemente um wrapper simples:
// client/retryHandler.ts
import axios from 'axios';
const MAX_RETRIES = 3;
const BASE_DELAY = 1000;
export async function withRetry<T>(
fn: () => Promise<T>,
retries = MAX_RETRIES
): Promise<T> {
try {
return await fn();
} catch (error) {
if (retries === 0 || !axios.isAxiosError(error)) {
throw error;
}
const delay = BASE_DELAY * Math.pow(2, MAX_RETRIES - retries);
await new Promise((resolve) => setTimeout(resolve, delay));
return withRetry(fn, retries - 1);
}
}
5. Métodos de alto nível para recursos da API
Cada recurso da API vira uma classe que recebe o ApiClient como dependência. Métodos assíncronos tipados expõem a funcionalidade completa com parâmetros de filtro e paginação.
// resources/Users.ts
import { ApiClient } from '../client/ApiClient';
import { PaginatedResponse } from '../types/api';
import { User, CreateUserPayload, UpdateUserPayload } from '../types/users';
export class Users {
constructor(private client: ApiClient) {}
async list(params?: {
page?: number;
perPage?: number;
search?: string;
}): Promise<PaginatedResponse<User>> {
return this.client.get<PaginatedResponse<User>>('/users', { params });
}
async get(id: string): Promise<User> {
return this.client.get<User>(`/users/${id}`);
}
async create(payload: CreateUserPayload): Promise<User> {
return this.client.post<User>('/users', payload);
}
async update(id: string, payload: UpdateUserPayload): Promise<User> {
return this.client.put<User>(`/users/${id}`, payload);
}
async delete(id: string): Promise<void> {
return this.client.delete<void>(`/users/${id}`);
}
async batchCreate(users: CreateUserPayload[]): Promise<User[]> {
return this.client.post<User[]>('/users/batch', { users });
}
}
6. Gerenciamento de autenticação e tokens
Para APIs que usam OAuth2, implemente um fluxo de login que retorna tokens de acesso e refresh. O SDK deve renovar tokens expirados automaticamente.
// auth/OAuth2Client.ts
import { ApiClient } from '../client/ApiClient';
interface TokenResponse {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
export class OAuth2Client {
private client: ApiClient;
private accessToken: string = '';
private refreshToken: string = '';
private expiresAt: number = 0;
constructor(baseURL: string) {
this.client = new ApiClient({ baseURL });
}
async login(email: string, password: string): Promise<void> {
const response = await this.client.post<TokenResponse>('/auth/login', {
email,
password,
});
this.setTokens(response);
}
private setTokens(response: TokenResponse): void {
this.accessToken = response.accessToken;
this.refreshToken = response.refreshToken;
this.expiresAt = Date.now() + response.expiresIn * 1000;
this.client.config.accessToken = this.accessToken;
}
async refreshAccessToken(): Promise<void> {
const response = await this.client.post<TokenResponse>('/auth/refresh', {
refreshToken: this.refreshToken,
});
this.setTokens(response);
}
isTokenExpired(): boolean {
return Date.now() >= this.expiresAt;
}
}
7. Testes e documentação do SDK
Testes unitários com Jest e nock garantem que o SDK funciona sem depender do servidor real.
// tests/Users.test.ts
import nock from 'nock';
import { ApiClient } from '../src/client/ApiClient';
import { Users } from '../src/resources/Users';
const BASE_URL = 'https://api.example.com/v2';
describe('Users resource', () => {
let client: ApiClient;
let users: Users;
beforeEach(() => {
client = new ApiClient({ baseURL: BASE_URL, apiKey: 'test-key' });
users = new Users(client);
});
afterEach(() => {
nock.cleanAll();
});
it('should list users with pagination', async () => {
const mockResponse = {
data: [{ id: '1', name: 'John', email: 'john@test.com', createdAt: '2024-01-01' }],
total: 1,
page: 1,
perPage: 10,
hasMore: false,
};
nock(BASE_URL)
.get('/users')
.query({ page: 1, perPage: 10 })
.reply(200, mockResponse);
const result = await users.list({ page: 1, perPage: 10 });
expect(result.data).toHaveLength(1);
expect(result.total).toBe(1);
});
});
Para documentação, use TypeDoc para gerar HTML a partir dos comentários JSDoc:
// Comentário JSDoc para método público
/**
* Cria um novo usuário.
* @param payload - Dados do usuário a ser criado
* @returns O usuário recém-criado
* @throws {ApiError} Se os dados forem inválidos
*/
async create(payload: CreateUserPayload): Promise<User> {
return this.client.post<User>('/users', payload);
}
8. Publicação e manutenção contínua
Configure scripts no package.json para build e publicação. Use semantic-release para automatizar changelogs e versionamento.
// package.json (trecho relevante)
{
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint src/",
"prepublishOnly": "npm run build && npm test",
"semantic-release": "semantic-release"
},
"files": ["dist/"]
}
Para CI, um workflow GitHub Actions simples:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
Estratégias de depreciação: quando um método for obsoleto, use o decorator @deprecated no JSDoc e mantenha-o por pelo menos duas versões menores antes de removê-lo.
Referências
- TypeScript Documentation: Project Configuration — Guia oficial sobre configuração de tsconfig.json para bibliotecas e strict mode.
- Axios: Interceptors Documentation — Documentação oficial sobre interceptors de requisição e resposta para autenticação e tratamento de erros.
- Jest: Testing Asynchronous Code — Tutoriais oficiais sobre testes assíncronos e mocks em Jest.
- TypeDoc: Documentation Generator — Ferramenta oficial para gerar documentação a partir de comentários JSDoc em TypeScript.
- Semantic Release: Automated Version Management — Guia completo para automatizar versionamento semântico e changelogs no npm.
- nock: HTTP Mocking Library — Biblioteca para simular requisições HTTP em testes unitários, ideal para testar SDKs sem depender de servidores reais.