Skip to content

Frontend Migration Guide - HTTP Contract V1 (Gryd)

Este documento descreve o contrato HTTP oficial para V1 e o de/para completo para atualizar o framework frontend sem alterar versão de API.

1. Premissas

  • A versão permanece V1.
  • A base URL permanece /api/v1.
  • A mudança é de shape de request/response e de padronização de erro, não de versionamento.
  • O backend segue:
    • Sucesso: payload direto (T, PagedResult<T>, ou vazio em 204).
    • Erro: ProblemDetails (application/problem+json).

2. Decisão de contrato (resumo executivo)

Antes

  • Muitos endpoints retornavam envelope interno Result<T> no HTTP (isSuccess, data, errorMessage, errorCode, errors).
  • Erros variavam entre envelope e formatos diferentes.

Agora (padrão único)

  • Sucesso HTTP: payload direto, sem envelope.
  • Erro HTTP: ProblemDetails com status/title/detail e extensions padronizadas.

3. Contrato de sucesso (V1)

3.1 Entidade única

  • 200 OK
  • Body: T
json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "John Doe"
}

3.2 Listagem paginada

  • 200 OK
  • Body: PagedResult<T>
json
{
  "items": [{ "id": "..." }],
  "totalCount": 150,
  "pageNumber": 1,
  "pageSize": 20,
  "totalPages": 8,
  "hasNext": true,
  "hasPrevious": false
}

3.3 Criação

  • 201 Created
  • Header: Location
  • Body: T criado

3.4 Update

  • 200 OK
  • Body: T atualizado

3.5 Delete

  • 204 No Content
  • Sem body

3.6 Comandos sem payload de retorno

  • 200 OK (body pode estar vazio)
  • ou 204 No Content quando aplicável

4. Contrato de erro (ProblemDetails)

4.1 Shape canônico

json
{
  "type": "https://gryd.io/errors/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "User not found",
  "instance": "/api/v1/users/550e8400-e29b-41d4-a716-446655440000",
  "traceId": "00-2d6a...",
  "code": "USER_NOT_FOUND",
  "errors": {
    "email": ["Email is required"]
  }
}

4.2 Campos relevantes para frontend

  • detail: mensagem principal para UX.
  • code: chave de regra de negócio (branching de UI).
  • errors: erros detalhados (array, objeto por campo, ou estrutura serializada).
  • traceId: correlação com logs/suporte.

4.3 Mapeamento de status por sufixo de code

  • *_NOT_FOUND -> 404
  • *_ALREADY_EXISTS, *_ALREADY_ASSIGNED, *_CONFLICT -> 409
  • *_FORBIDDEN, *_ACCESS_DENIED -> 403
  • *_UNAUTHORIZED -> 401
  • *_UNPROCESSABLE -> 422
  • sem sufixo reconhecido -> 400

4.4 Codigos canonicos de autenticacao/autorizacao

  • INVALID_CREDENTIALS -> 401
  • TOKEN_MISSING, TOKEN_INVALID, TOKEN_EXPIRED, TOKEN_REVOKED, SESSION_INVALIDATED -> 401
  • REFRESH_TOKEN_INVALID, REFRESH_TOKEN_EXPIRED, REFRESH_TOKEN_REVOKED -> 401
  • AUTH_RISK_FORBIDDEN -> 403 (bloqueio por politica de risco/zero-trust)

5. De/Para (frontend)

5.1 Envelope de sucesso

AntesAgora
response.isSuccessusar status HTTP (2xx)
response.databody já é o payload final
response.errorMessageproblem.detail
response.errorCodeproblem.code
response.errorsproblem.errors

5.2 Endpoints CRUD (5 padrões)

EndpointAntesAgora
GET /resourceResult<PagedResult<T>>PagedResult<T>
GET /resource/{id}Result<T>T
POST /resourceResult<T>201 + T + Location
PUT /resource/{id}Result<T>T
DELETE /resource/{id}Result204 sem body

5.3 Erros

AntesAgora
formatos mistosProblemDetails único
checagem por isSuccess=falsechecagem por status HTTP >= 400
parsing errorMessage/errors variávelparsing padrão detail/code/errors/traceId

6. Requests padrão para cenários não-CRUD simples

Para módulos avançados (bulk, workflows, filtros ricos), o backend disponibiliza contratos padrão.

6.1 PagedAndSortedRequest

json
{
  "page": 1,
  "pageSize": 20,
  "sortBy": "createdAt",
  "sortDirection": "Ascending",
  "search": "john",
  "includeDeleted": false
}

6.2 BulkRequest<T>

json
{
  "items": [
    { "id": "..." }
  ]
}

6.3 OperationResponse<T> (opcional para workflows)

json
{
  "data": {},
  "message": "Operation completed",
  "code": "OP_SUCCESS",
  "meta": {}
}

6.4 BulkOperationResponse

json
{
  "total": 10,
  "succeeded": 8,
  "failed": 2,
  "failures": [
    { "itemKey": "u-1", "reason": "Already assigned", "code": "USER_ALREADY_ASSIGNED" }
  ]
}

7. Contratos TypeScript recomendados

ts
export type ApiSuccess<T> = T;

export interface ProblemDetailsRaw {
  type?: string;
  title?: string;
  status?: number;
  detail?: string;
  instance?: string;
  traceId?: string; // extension
  code?: string;    // extension
  errors?: unknown; // extension
  [key: string]: unknown;
}

export interface ApiError {
  status: number;
  message: string;
  code?: string;
  traceId?: string;
  errors?: unknown;
  raw: ProblemDetailsRaw;
}

export interface PagedResult<T> {
  items: T[];
  totalCount: number;
  pageNumber: number;
  pageSize: number;
  totalPages: number;
  hasNext: boolean;
  hasPrevious: boolean;
}

export interface PagedAndSortedRequest {
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortDirection?: "Ascending" | "Descending";
  search?: string;
  includeDeleted?: boolean;
}

export interface BulkRequest<T> {
  items: T[];
}

export interface OperationResponse<T> {
  data: T;
  message?: string;
  code?: string;
  meta?: Record<string, unknown>;
}

export interface BulkItemFailure {
  itemKey: string;
  reason: string;
  code?: string;
}

export interface BulkOperationResponse {
  total: number;
  succeeded: number;
  failed: number;
  failures: BulkItemFailure[];
}

8. Adapter HTTP (obrigatório no framework frontend)

8.1 Regra de sucesso

  • 2xx: retornar body direto.
  • 204: retornar undefined.

8.2 Regra de erro

  • >= 400: normalizar para ApiError lendo ProblemDetails.

8.3 Exemplo axios

ts
import axios, { AxiosError } from "axios";

export const api = axios.create({
  baseURL: "/api/v1",
});

function normalizeError(error: AxiosError): ApiError {
  const raw = (error.response?.data ?? {}) as ProblemDetailsRaw;

  return {
    status: raw.status ?? error.response?.status ?? 500,
    message: raw.detail ?? raw.title ?? "Unexpected error",
    code: raw.code as string | undefined,
    traceId: raw.traceId as string | undefined,
    errors: raw.errors,
    raw,
  };
}

api.interceptors.response.use(
  (response) => response,
  (error: AxiosError) => Promise.reject(normalizeError(error))
);

8.4 Exemplo fetch wrapper

ts
export async function request<T>(input: RequestInfo, init?: RequestInit): Promise<T | undefined> {
  const response = await fetch(input, init);

  if (response.status === 204) return undefined;

  const hasBody = response.headers.get("content-length") !== "0";
  const payload = hasBody ? await response.json().catch(() => undefined) : undefined;

  if (response.ok) return payload as T;

  const raw = (payload ?? {}) as ProblemDetailsRaw;
  throw {
    status: raw.status ?? response.status,
    message: raw.detail ?? raw.title ?? "Unexpected error",
    code: raw.code,
    traceId: raw.traceId,
    errors: raw.errors,
    raw,
  } satisfies ApiError;
}

9. Estratégia para formulários (validação)

errors pode chegar como:

  • array de mensagens
  • objeto por campo ({ field: [msg1, msg2] })
  • string serializada

Recomendação:

  1. Criar normalizador mapFieldErrors(errors: unknown): Record<string, string[]>.
  2. Exibir detail como mensagem geral.
  3. Exibir mensagens por campo quando errors tiver estrutura compatível.

10. Impacto no framework frontend (itens obrigatórios)

  1. Remover parser de envelope Result<T> em sucesso.
  2. Alterar client base para usar payload direto.
  3. Centralizar normalização de ProblemDetails.
  4. Atualizar adapters de CRUD para aceitar 204 sem body.
  5. Mapear code para regras de UX (ex.: conflito, not found).
  6. Usar traceId em logging/tela de suporte.
  7. Atualizar SDK/mocks/testes de contrato.
  8. Revisar handlers de React Query/SWR para não acessar response.data.data.
  9. Revisar middlewares globais de erro (toasts, tracking, retry).
  10. Manter sempre a base URL em /api/v1.

11. De/Para de código (exemplos rápidos)

Antes

ts
const res = await api.get<Result<UserDto>>(`/users/${id}`);
if (!res.data.isSuccess) throw new Error(res.data.errorMessage);
return res.data.data;

Agora

ts
const res = await api.get<UserDto>(`/users/${id}`);
return res.data;

Delete

ts
await api.delete(`/users/${id}`); // sucesso esperado: 204

Erro

ts
try {
  await api.post(`/users`, payload);
} catch (e) {
  const err = e as ApiError;
  console.error(err.code, err.message, err.traceId, err.errors);
}

12. Matriz de QA para front

Executar pelo menos:

  1. CRUD completo (list/get/create/update/delete) em módulo de referência.
  2. Fluxo com erro 404, 409, 403, 401, 422, 400.
  3. Formulário com erro de validação e mapeamento por campo.
  4. Telas com paginação (PagedResult<T>).
  5. Rotas de comando sem payload e com 204.
  6. Logs com traceId disponíveis no erro.

13. Conclusão

  • O frontend passa a consumir um contrato HTTP mais REST e previsível.
  • O backend mantém V1 e /api/v1.
  • A principal adaptação é remover dependência do envelope Result<T> e padronizar tratamento de ProblemDetails.

Updated at:

Released under the MIT License.