Appearance
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 em204). - Erro:
ProblemDetails(application/problem+json).
- Sucesso: payload direto (
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:
ProblemDetailscomstatus/title/detaileextensionspadronizadas.
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:
Tcriado
3.4 Update
200 OK- Body:
Tatualizado
3.5 Delete
204 No Content- Sem body
3.6 Comandos sem payload de retorno
200 OK(body pode estar vazio)- ou
204 No Contentquando 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->401TOKEN_MISSING,TOKEN_INVALID,TOKEN_EXPIRED,TOKEN_REVOKED,SESSION_INVALIDATED->401REFRESH_TOKEN_INVALID,REFRESH_TOKEN_EXPIRED,REFRESH_TOKEN_REVOKED->401AUTH_RISK_FORBIDDEN->403(bloqueio por politica de risco/zero-trust)
5. De/Para (frontend)
5.1 Envelope de sucesso
| Antes | Agora |
|---|---|
response.isSuccess | usar status HTTP (2xx) |
response.data | body já é o payload final |
response.errorMessage | problem.detail |
response.errorCode | problem.code |
response.errors | problem.errors |
5.2 Endpoints CRUD (5 padrões)
| Endpoint | Antes | Agora |
|---|---|---|
GET /resource | Result<PagedResult<T>> | PagedResult<T> |
GET /resource/{id} | Result<T> | T |
POST /resource | Result<T> | 201 + T + Location |
PUT /resource/{id} | Result<T> | T |
DELETE /resource/{id} | Result | 204 sem body |
5.3 Erros
| Antes | Agora |
|---|---|
| formatos mistos | ProblemDetails único |
checagem por isSuccess=false | checagem por status HTTP >= 400 |
parsing errorMessage/errors variável | parsing 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: retornarundefined.
8.2 Regra de erro
>= 400: normalizar paraApiErrorlendoProblemDetails.
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:
- Criar normalizador
mapFieldErrors(errors: unknown): Record<string, string[]>. - Exibir
detailcomo mensagem geral. - Exibir mensagens por campo quando
errorstiver estrutura compatível.
10. Impacto no framework frontend (itens obrigatórios)
- Remover parser de envelope
Result<T>em sucesso. - Alterar client base para usar payload direto.
- Centralizar normalização de
ProblemDetails. - Atualizar adapters de CRUD para aceitar
204sem body. - Mapear
codepara regras de UX (ex.: conflito, not found). - Usar
traceIdem logging/tela de suporte. - Atualizar SDK/mocks/testes de contrato.
- Revisar handlers de React Query/SWR para não acessar
response.data.data. - Revisar middlewares globais de erro (toasts, tracking, retry).
- 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: 204Erro
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:
- CRUD completo (list/get/create/update/delete) em módulo de referência.
- Fluxo com erro
404,409,403,401,422,400. - Formulário com erro de validação e mapeamento por campo.
- Telas com paginação (
PagedResult<T>). - Rotas de comando sem payload e com
204. - Logs com
traceIddisponí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 deProblemDetails.