Executando verificação de segurança...
1

API Layers em Aplicações Fullstack: O Código que Falta entre Fetch e Produção

Resumo rápido

Este post cobre a lacuna que existe entre fazer um fetch funcionar e ter uma camada de API que sobrevive a 18 meses de evolução de produto. Vou mostrar como estruturo as camadas de comunicação em projetos fullstack com TypeScript, tanto no lado do cliente (React/Next.js) quanto no servidor (Node.js/Express/Fastify), com código funcional, tipagem compartilhada e tratamento de erro que não depende de try/catch espalhado em 47 arquivos.

O problema que ninguém estrutura direito

Em 2021, assumi um projeto Next.js com 14 meses de desenvolvimento acumulado. O time tinha 6 desenvolvedores. Encontrei 83 chamadas fetch espalhadas por componentes React, cada uma com seu próprio try/catch, seu próprio formato de erro, sua própria lógica de retry. Algumas convertiam a resposta com .json(), outras com .text() e depois JSON.parse. Sete delas não tratavam status 4xx como erro. O tempo médio para adicionar um novo endpoint consumido no frontend era de 45 minutos, incluindo debugging de tipagem.

Depois de reestruturar as camadas de API com o padrão que vou descrever, esse tempo caiu para 8 minutos. Erros de serialização em produção foram de ~12 por semana para zero nos 4 meses seguintes.

A raiz do problema: a maioria dos projetos fullstack JavaScript trata a comunicação HTTP como detalhe de implementação em vez de camada arquitetural. Ninguém desenha essa camada. Ela simplesmente acontece.

Anatomia das camadas

Eu organizo a comunicação em três camadas distintas, tanto no cliente quanto no servidor:

┌─────────────────────────────────────┐
│  Camada de Domínio (types/schemas)  │  ← compartilhada
├─────────────────────────────────────┤
│  Camada de Transporte (HTTP client) │  ← infra
├─────────────────────────────────────┤
│  Camada de Serviço (use cases)      │  ← lógica de negócio
└─────────────────────────────────────┘

A camada de domínio define os contratos. A camada de transporte sabe falar HTTP. A camada de serviço orquestra chamadas e transforma dados. Nenhum componente React ou controller Express toca HTTP diretamente.

Camada 1: Contratos compartilhados com Zod

O ponto de partida é um pacote (ou diretório) de schemas que tanto o frontend quanto o backend importam. Uso Zod porque ele faz validação em runtime e inferência de tipo em compile time ao mesmo tempo.

// packages/contracts/src/user.ts
import { z } from "zod";

export const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  displayName: z.string().min(1).max(120),
  role: z.enum(["admin", "member", "viewer"]),
  createdAt: z.string().datetime(), // ISO 8601 como string, não Date
});

// Inferência direta evita manter tipo e schema em sincronia manual
export type User = z.infer<typeof UserSchema>;

export const CreateUserPayload = UserSchema.omit({
  id: true,
  createdAt: true,
});
export type CreateUserPayload = z.infer<typeof CreateUserPayload>;

// Schema de resposta paginada reutilizável
export const PaginatedResponse = <T extends z.ZodType>(itemSchema: T) =>
  z.object({
    items: z.array(itemSchema),
    total: z.number().int().nonneg(),
    page: z.number().int().positive(),
    pageSize: z.number().int().positive(),
  });

export const PaginatedUsersSchema = PaginatedResponse(UserSchema);
export type PaginatedUsers = z.infer<typeof PaginatedUsersSchema>;

Por que createdAt é string e não Date? Porque JSON não tem tipo Date. Se você colocar z.coerce.date(), o schema funciona na validação mas quebra a serialização quando o backend responde e o frontend parseia. Manter como ISO string e converter para Date apenas onde a UI precisa elimina uma classe inteira de bugs.

Se você usa monorepo com arquitetura bem definida, esse pacote contracts fica como dependência interna do workspace.

Camada 2: HTTP Client tipado no frontend

A segunda camada encapsula toda a mecânica HTTP. Eu construo um wrapper fino so


Leia o artigo completo em https://vivodecodigo.com.br/backend/api-layers-fullstack-javascript-fetch-tipagem-ponta-a-ponta

Carregando publicação patrocinada...