Gerar serviços do OpenAPI (tempest gen api)
Se o seu backend é FastAPI (ou qualquer API que exponha um OpenAPI 3.x), o
tempest gen api lê o spec e gera o cliente tipado pra você — no padrão da
Tempest: schemas Zod, tipos TS e uma classe de serviço por grupo de
rotas (tag), com validação de entrada via Zod.
Nada de escrever fetch na mão nem manter tipos sincronizados com o backend.
npx tempest gen api http://127.0.0.1:8000/openapi.json --out src/api
# ou a partir de um arquivo:
npx tempest gen api ./openapi.json --out src/api
O que é gerado
Para cada grupo de rotas (a tag da operação no OpenAPI) sai uma pasta com
três arquivos:
src/api/
├── index.ts # re-exporta tudo
├── users/
│ ├── schemas.ts # schemas Zod (z.object…) de cada model do grupo
│ ├── types.ts # tipos TS (z.infer dos schemas)
│ └── service.ts # classe UsersService com 1 método por rota
└── products/
├── schemas.ts
├── types.ts
└── service.ts
schemas.ts — Zod por model
import { z } from "zod";
export const UserSchema = z.object({
id: z.number().int(),
email: z.string().email(),
role: z.enum(["admin", "member"]).optional(),
bio: z.string().nullable().optional(),
});
export const UserCreateSchema = z.object({
email: z.string().email(),
name: z.string().optional(),
});
types.ts — tipos inferidos
import type { z } from "zod";
import * as S from "./schemas";
export type User = z.infer<typeof S.UserSchema>;
export type UserCreate = z.infer<typeof S.UserCreateSchema>;
service.ts — classe por grupo, com validação
import type { ApiClient } from "tempest-react-sdk";
import { encodeIdSegment } from "tempest-react-sdk";
import * as S from "./schemas";
import type { User, UserCreate } from "./types";
export class UsersService {
constructor(private readonly api: ApiClient) {}
/** `GET /users` */
async listUsers(params: { limit?: number }): Promise<User[]> {
return this.api.get<User[]>("/users", { params });
}
/** `POST /users` */
async createUser(body: UserCreate): Promise<User> {
S.UserCreateSchema.parse(body); // valida a entrada em runtime
return this.api.post<User>("/users", { body });
}
/** `GET /users/{id}` */
async getUser(id: number): Promise<User> {
return this.api.get<User>(`/users/${encodeIdSegment(id)}`);
}
}
Parâmetro de path vira um segmento só
Repare no encodeIdSegment(id) acima: todo parâmetro de path passa por ele
antes de entrar na URL. O valor do id costuma vir de fora do app — um param do
React Router, um search param, um valor que outro usuário gravou. Se ele
entrasse cru, "../../account" levaria o DELETE /users/{id} para
DELETE /account, com o Bearer do usuário logado, e "7?role=admin"
injetaria query string. É a mesma proteção que o
createDataProvider aplica desde a 0.77.0:
| Valor passado | Requisição enviada |
|---|---|
7 |
GET /users/7 |
"../../account" |
GET /users/..%2F..%2Faccount |
"7?role=admin" |
GET /users/7%3Frole%3Dadmin |
"7#x" |
GET /users/7%23x |
"", ".", "..", null, undefined |
nenhuma — lança TypeError antes do fetch |
Parâmetro tipado como boolean, array ou objeto também passa pelo
encodeIdSegment (com uma asserção de tipo, já que ele aceita
string | number): true vira true, ["a", "b"] vira a%2Cb.
Regenere para receber a proteção
Serviços gerados antes desta versão interpolam o parâmetro cru. Rode
tempest gen api de novo — nenhuma assinatura de método muda.
Como usar
A classe recebe um ApiClient (do createApiClient) no
construtor — você injeta o seu, com a base URL e o token:
import { createApiClient } from "tempest-react-sdk";
import { UsersService } from "@/api/users";
import { useAuth } from "@/stores/auth";
const api = createApiClient({
baseURL: import.meta.env.VITE_API_URL,
getToken: () => useAuth.getState().token,
});
export const users = new UsersService(api);
// users.listUsers({ limit: 20 }) → Promise<User[]>
// users.createUser({ email: "a@b.c" }) → valida com Zod, depois POST
Com o TanStack Query (já montado pelo AppProviders):
import { useQuery } from "@tanstack/react-query";
import { users } from "@/api";
function UserList() {
const { data } = useQuery({ queryKey: ["users"], queryFn: () => users.listUsers({}) });
// data: User[] | undefined
}
Re-gere quando o backend mudar
Rode tempest gen api de novo após mexer no backend — os arquivos em
src/api/ são gerados, então trate-os como build artifact (não edite à
mão; versione se quiser, mas regenere ao mudar o contrato).
A saída fica dentro do projeto
Antes da primeira escrita, o gen api confere o --out, cada pasta de grupo
e cada arquivo: symlink (inclusive pendente) no lugar de qualquer um deles,
caminho fora do projeto e ancestral que é symlink para fora são recusados, e
nada é escrito. Um src/api que é link num repo clonado não leva os arquivos
gerados para outro lugar do disco.
Validação de entrada, tipagem na saída
O método valida o body com Schema.parse(...) (erro cedo se você mandar
dado inválido) e tipa a resposta. A resposta não é validada em runtime
por padrão — confie no contrato do backend, ou valide manualmente com o
schema correspondente se precisar de garantia.
Suporte atual
v1 cobre o comum de specs FastAPI: components.schemas (object, enum, array,
$ref, nullable, anyOf/oneOf/allOf), parâmetros de path/query, body e
resposta JSON, agrupados pela primeira tag. YAML e múltiplos content-types
ainda não — aponte para o /openapi.json (JSON).
Spec de terceiro não vira código
Todo texto do spec entra no TypeScript gerado escapado pela posição onde cai:
tag, summary e path em comentário não fecham o /** */, segmentos do path
viram texto do template literal, pattern vira new RegExp("..."). Nome que
precisa ser identificador e não é — {user-id}, user.Response,
User-Out — vira um identificador válido: camelCase para parâmetro
(userId), PascalCase para schema e tipo (UserResponse), prefixo _ para
palavra reservada ou dígito inicial (_string, _2fa). Onde o nome importa
em runtime, vale o original: a URL monta {user-id} com o valor de userId,
e chave de objeto sai como string escapada ("display-name"). Nome que já é
identificador fica como está. Dois nomes que viram o mesmo identificador
(User-Out e user.Out) são recusados com erro que cita os dois, e um
{placeholder} do path que não é parâmetro declarado também — não há
variável segura para ele.
O mesmo vale para nomes que o próprio gerador já usa. Parâmetro de path que
cairia em body, params, S ou encodeIdSegment vira
bodyParam/paramsParam/SParam/encodeIdSegmentParam; operationId
constructor ou api vira constructor2/api2; schema chamado
ApiClient, OffsetPage, CursorPage, Promise, Record, z, S ou
encodeIdSegment ganha prefixo _; e duas tags cujo nome no barrel coincide (user 1 e
user1) são recusadas citando as duas. Um pattern que não compila como
regex JavaScript falha a geração, citando o schema e o caminho da
propriedade — em vez de quebrar o app no import.
Recap
tempest gen api <url|file> --out src/apigera, por grupo de rotas, três arquivos:schemas.ts(Zod),types.ts(z.infer) eservice.ts(classe).- A classe de serviço recebe um
ApiClientinjetado e valida o body com Zod antes de chamar a API. - Todo parâmetro de path passa por
encodeIdSegment: fica num segmento só, e""/"."/".."lançam antes da requisição. - Trate
src/api/como artefato gerado — regenere quando o contrato mudar. - Veja também: HTTP /
createApiClient· Query · CLItempest.