@vitongovisk/forge
v1.5.0
Published
> **Opinionated frontend architecture generator for TypeScript projects.** > Versão `2.0` · Publicado como `@vitongovisk/forge`
Maintainers
Readme
⚒️ Forge CLI
Opinionated frontend architecture generator for TypeScript projects.
Versão2.0· Publicado como@vitongovisk/forge
Forge é um CLI que gera automaticamente todas as camadas de integração com APIs REST no seu frontend: tipos de contrato, cliente HTTP, serviços e hooks TanStack Query — tudo fortemente tipado, a partir de um único comando.
Pré-requisitos
Seu projeto deve ter as seguintes dependências instaladas:
npm install axios @tanstack/react-queryO Forge verifica essas dependências no
forge inite oferece instalação automática caso não estejam presentes.
Node.js: >= 18
Instalação
npm install -D @vitongovisk/forgeOu use globalmente:
npm install -g @vitongovisk/forgeInício Rápido
# 1. Inicializar o Forge no projeto
forge init
# 2. Criar um módulo
forge module users
# 3. Adicionar endpoints ao módulo
forge add users getUser --get
forge add users createUser --post
forge add users updateUser --put
forge add users deleteUser --deletePronto. O Forge gera toda a estrutura de arquivos para você.
Multi-Backend
Se o seu projeto consome mais de uma API (ex: API principal + serviço de pagamentos + serviço de analytics), use o comando forge client para registrar instâncias Axios adicionais:
# 1. Registrar um novo client (instância Axios)
forge client payments
# → injeta `paymentsApi` no api-client.ts e registra em forge.data.json
# 2. Criar um módulo que consome esse client
forge module billing --client paymentsApi
# 3. Adicionar endpoints — herda o client do módulo automaticamente
forge add billing chargeUser --post # usa paymentsApi
forge add billing refund --post --client api # override por endpointUsuários single-backend (só api) não veem essas opções — o fluxo continua idêntico ao Quick Start. Detalhes de cada comando abaixo.
Comandos
forge init
Inicializa o Forge no projeto atual.
forge init
forge init --overwrite # Reconfigura um projeto já inicializadoO que faz:
- Cria o arquivo de configuração
.forge.json(inclui a versão do Forge usada —forgeVersion— pra suportar migrações futuras) - Cria
forge.data.json(registro de intenção dos módulos, comclients: ["api"]por padrão) - Cria
forge.state.json(estado real dos arquivos gerados) - Gera o
api-client.tscom Axios configurado (interceptors de autenticação JWT e unwrap deresponse.data) - Gera os tipos base compartilhados (
api.types.ts,global.types.ts,shared.types.ts) - Gera o utilitário
api.utils.tscomparseApiError
Pergunta apenas: modo de sincronização (auto ou manual).
forge module <name> · alias: forge m
Cria um novo módulo com a estrutura base completa.
forge module users
forge m auth
# Módulo sem tipos inicialmente (para módulos utilitários como auth, session)
forge module auth --minimal
# Multi-backend: especificar qual API client o módulo consome
forge module billing --client paymentsApiOpções:
| Flag | Descrição |
|---|---|
| --minimal | Pula a criação de <module>.types.ts e contracts/ |
| --client <name> | Declara qual instância Axios o módulo consome (default: api). Valida contra os clients registrados em forge.data.json |
| --sync / --no-sync | Força ou pula a sincronização automática após criação |
Se houver mais de um client registrado e --client não for passado, o Forge entra em modo interativo perguntando qual usar.
Estrutura gerada (padrão):
src/modules/users/
├── users.api.ts ← cliente HTTP tipado
├── users.service.ts ← camada de serviço/negócio
└── users.types.ts ← interface base do modeloCom --minimal (sem users.types.ts e sem contracts/):
src/modules/auth/
├── auth.api.ts
└── auth.service.tsforge client <name> · alias: forge c
Registra uma nova instância Axios no api-client.ts, permitindo que módulos consumam APIs diferentes (ex: serviço de pagamentos, analytics, autenticação separada).
forge client payments
forge c analyticsO que faz:
- Injeta no
api-client.ts(via AST, preserva edições manuais existentes):export const paymentsApi = axios.create({ baseURL: import.meta.env.VITE_API_PAYMENTS_URL, timeout: 1000 * 15, }); setupInterceptors(paymentsApi); - Registra
"paymentsApi"emforge.data.json→clients. - Roda
syncautomaticamente (sesync.mode === "auto").
Convenção de nomes: você passa o nome base (ex: payments), o Forge cria a instância com sufixo Api (ex: paymentsApi). A env var segue o padrão VITE_API_<UPPER_SNAKE>_URL (ex: VITE_API_PAYMENTS_URL).
Validação: ao usar forge module --client X ou forge add --client X, o nome X precisa estar registrado em forge.data.json. Caso contrário, o Forge erra com a lista dos clients disponíveis.
forge add <module> <functionName> · alias: forge a
Adiciona um endpoint a um módulo existente, gerando todas as camadas verticalmente.
# Com flag de método explícita
forge add users getUser --get
forge add users createUser --post
forge add users updateUser --put
forge add users deleteUser --delete
# Aliases
forge a users getUser --get
# Sem flag → modo interativo
forge add users getUser
# → Escolha o método:
# 1. GET
# 2. POST
# 3. PUT
# 4. DELETE
# Override por endpoint (multi-backend)
forge add billing refund --post --client api
# → o módulo "billing" usa paymentsApi por default, mas esse
# endpoint específico vai falar com o client "api".
# O Forge adiciona o import de "api" no billing.api.ts
# automaticamente, sem quebrar os outros endpoints.Resolução do client (em ordem de prioridade):
- Flag
--client X(se passada e válida). - Client salvo no módulo (
forge.data.json). - Único client registrado (sem prompt).
- Prompt interativo (se houver >1 client e o módulo ainda não tem um salvo — auto-heal: salva no módulo após a escolha).
Convenção de URLs: módulos e ações são convertidos para kebab-case ao gerar o path HTTP:
| Comando | URL gerado |
|---|---|
| forge add userProfile createUser --post | POST /user-profile/create-user |
| forge add billing chargeUser --post | POST /billing/charge-user |
| forge add userProfile getUser --get | GET `/user-profile/${payload.params.id}` |
| forge add billing refund --delete | DELETE `/billing/${payload.params.id}` |
GET, PUT e DELETE usam o id do payload (não o nome da função) — convenção REST padrão. POST usa /<módulo>/<ação> no kebab.
O que é gerado por chamada:
| Arquivo | Descrição |
|---|---|
| contracts/getUser.types.ts | Tipos de Payload, Response e mapa de erros da operação |
| Injeção em users.api.ts | Método getUser adicionado via AST ao objeto UsersAPI |
| Injeção em users.service.ts | Método getUser com try/catch adicionado ao UsersService |
| hooks/useGetUser.hook.ts | Hook useQuery ou useMutation do TanStack Query |
Estrutura final do módulo após 4 endpoints:
src/modules/users/
├── contracts/
│ ├── createUser.types.ts
│ ├── deleteUser.types.ts
│ ├── getUser.types.ts
│ ├── updateUser.types.ts
│ └── index.ts ← barrel: export * de cada *.types
├── hooks/
│ ├── useCreateUser.hook.ts
│ ├── useDeleteUser.hook.ts
│ ├── useGetUser.hook.ts
│ ├── useUpdateUser.hook.ts
│ └── index.ts ← barrel: export * de cada hook
├── users.api.ts
├── users.service.ts
├── users.types.ts
└── index.ts ← barrel: api + service + types + hooks/ + contracts/Os arquivos index.ts (barrel exports) são regenerados do zero a cada add/remove/rename — listam alfabeticamente o que existe no disco. Resultado:
// Em vez de:
import { useGetUser } from '@/modules/users/hooks/useGetUser.hook';
import { UsersService } from '@/modules/users/users.service';
import { type GetUserPayload } from '@/modules/users/contracts/getUser.types';
// Você pode:
import { useGetUser, UsersService, type GetUserPayload } from '@/modules/users';Importante: os index.ts são overwritten a cada operação. Se você editar à mão pra customizar (ex: renomear um re-export), perde na próxima geração. Pra módulos --minimal, o index.ts do módulo omite a linha de types; hooks/index.ts e contracts/index.ts só existem quando há arquivos a re-exportar (são deletados se todos os endpoints forem removidos).
forge sync · alias: forge s
Sincroniza o estado interno do Forge com o estado real do filesystem.
forge syncO que faz:
- Verifica quais arquivos existem fisicamente no disco
- Atualiza o
forge.state.jsoncom o estado real - Detecta módulos incompletos (api ou service faltando)
- Detecta módulos órfãos (existem no disco mas não foram registrados)
- Parseia
api-client.tse cruza comdata.clients:- missing — em
data.clientsmas sumiu do arquivo - órfão — no arquivo mas não em
data.clients
- missing — em
- Detecta módulos apontando pra clients que sumiram (link broken)
- Exibe um relatório detalhado no terminal
Relatório de exemplo (estado degradado):
🔄 Sincronizando projeto...
✅ Sync finalizado!
📊 Módulos: 2 ativos, 1 incompletos, 1 órfãos
🔌 Clients: 1 ativos, 1 missing, 1 órfãos
⚠️ Módulos com arquivos faltando:
- billing (faltando: service, contracts)
👻 Módulos órfãos detectados:
- legacy-module
💡 Dica: rode 'forge module legacy-module' para registrá-lo.
⚠️ Clients missing (registrados em data, sumiram do api-client.ts):
- paymentsApi
💡 Dica: rode 'forge client payments' pra recriar.
👻 Clients órfãos (no api-client.ts, mas não registrados):
- analyticsApi
💡 Dica: rode 'forge client analytics' pra registrá-los.
💥 Módulos apontando pra clients inexistentes:
- billing → paymentsApi
💡 Dica: recrie o client ou edite o módulo pra usar outro client registrado.Os comandos forge module --client X e forge add --client X também fazem pré-check live contra o api-client.ts — se o client estiver registrado em data.clients mas tiver sumido do arquivo, o comando aborta com a mesma dica de forge client X pra recriar.
forge list · alias: forge l
Lista todos os módulos registrados com seu status.
forge list
forge lExemplo de saída:
📦 Módulos (3 total)
✅ users — 4 método(s)
✅ billing — 2 método(s)
⚠️ legacy ← incompleto (faltando: service, contracts)Status possíveis: active (tudo no lugar), missing (arquivo crítico faltando), orphan (existe no disco mas não está registrado em forge.data.json).
forge describe <module> · alias: forge d
Exibe um relatório detalhado sobre um módulo registrado: status, client, endpoints (com verbo HTTP detectado do api.ts), e a presença de cada arquivo.
forge describe billing
forge d billingExemplo de saída:
📦 Module: billing ✅ active
Client: paymentsApi
🔌 Endpoints (2):
POST chargeUser
POST refund (using api — override)
📁 Files:
✅ src/modules/billing/billing.api.ts
✅ src/modules/billing/billing.service.ts
✅ src/modules/billing/billing.types.ts
✅ src/modules/billing/contracts (2 files)
✅ src/modules/billing/hooks (2 files)Quando um endpoint usa um client diferente do default do módulo (override por endpoint), aparece o marcador (using X — override) na frente do método. Para módulos missing/orphan, o comando exibe dicas sobre como reconciliar.
forge remove <module> <functionName> · alias: forge rm
Remove um endpoint do módulo, inverso do forge add. Limpa:
- Arquivo
contracts/<fn>.types.ts - Arquivo
hooks/use<Fn>.hook.ts - Propriedade
<fn>do objeto<Module>APIem*.api.ts(via AST) - Propriedade
<fn>do objeto<Module>Serviceem*.service.ts(via AST) - Imports dos tipos do contrato em
*.api.tse*.service.ts - Imports de clients axios que ficaram órfãos (ex: se o endpoint era um override por
--client, oimport { api }é removido quando nenhum outro método usarapi) - Entrada em
forge.data.json→resources[].methods
forge remove user deleteSession
forge rm billing refund
# Pula a confirmação interativa (útil em scripts)
forge rm user teste1 -y
forge rm user teste1 --yesConfirmação: por default, lista os arquivos/propriedades que vão sumir e pede y/N antes de executar. Passa -y/--yes pra confirmar automaticamente.
Erros e edge cases:
- Módulo inexistente → erro com sugestão
forge list. - Endpoint inexistente → erro com sugestão
forge describe <module>. - Endpoint parcialmente presente (ex: arquivo no disco mas não em
data.json) → remove o que encontrar e atualizadata.json.
forge rename <module> <oldName> <newName> · alias: forge rn
Renomeia um endpoint em todas as camadas — propriedades em *.api.ts/*.service.ts, tipos do contrato (Payload, Response, ValidationError, ErrorMap), função do hook, paths de import, nome dos arquivos, e a entrada em forge.data.json. POST também tem a URL kebab atualizada (/<módulo>/<old-kebab> → /<módulo>/<new-kebab>); GET/PUT/DELETE usam ${payload.params.id} na URL e ficam intactos.
forge rename billing chargeUser chargeCustomer
forge rn user deleteSession revokeSession
# Pula a confirmação interativa
forge rn billing chargeUser chargeCustomer -yResolução: se o nome novo normalizar pro mesmo camelCase do antigo (ex: charge-user vs chargeUser), erro — nada a renomear.
Erros e edge cases:
- Módulo inexistente → erro com sugestão
forge list. oldNamenão existe em<module>.api.ts→ erro com sugestãoforge describe <module>.newNamejá existe → erro pedindo outro nome ouforge rm <module> <newName>antes.- Imports do contrato/hook em código de aplicação não são atualizados — só os arquivos que o Forge gerou. Se você importou tipos do contrato em outros lugares, atualize esses paths à mão (TypeScript vai apontar).
Arquivos de Configuração
.forge.json
Configuração principal do Forge (gerada pelo init, raramente editada manualmente).
{
"forgeVersion": "1.2.0",
"project": {
"architecture": "module",
"modulePath": "src/modules",
"apiFile": "src/modules/api/api-client.ts"
},
"sync": {
"mode": "auto"
}
}| Campo | Valores | Descrição |
|---|---|---|
| forgeVersion | string | Versão do Forge que criou/atualizou o projeto. Usado pra migrações futuras |
| architecture | "module" | Sempre modular no V2 |
| modulePath | string | Pasta raiz dos módulos |
| apiFile | string | Caminho do api-client.ts |
| sync.mode | "auto" | "manual" | Se auto, sync roda após cada operação |
forge.data.json
Registro de intenção — o que você declarou ao Forge que existe no projeto.
{
"clients": ["api", "paymentsApi"],
"resources": [
{
"name": "users",
"methods": ["getUser", "createUser", "updateUser", "deleteUser"],
"client": "api"
},
{
"name": "billing",
"methods": ["chargeUser", "refund"],
"client": "paymentsApi"
}
]
}| Campo | Descrição |
|---|---|
| clients | Instâncias Axios registradas (default ["api"], expandido via forge client) |
| resources[].client | Qual client esse módulo usa por padrão (passa pro scaffold + cada add) |
forge.state.json
Registro de realidade — o que o Forge verificou que existe no filesystem.
{
"project": { "architecture": "module", "lastSync": "2026-05-22T00:00:00.000Z" },
"sync": { "mode": "auto", "lastRun": "2026-05-22T00:00:00.000Z" },
"resources": [
{
"name": "users",
"status": "active",
"client": "api",
"files": {
"api": { "exists": true, "path": "src/modules/users/users.api.ts" },
"service": { "exists": true, "path": "src/modules/users/users.service.ts" },
"types": { "exists": true, "path": "src/modules/users/users.types.ts" },
"contracts": { "exists": true, "path": "src/modules/users/contracts" },
"hooks": { "exists": true, "path": "src/modules/users/hooks" }
},
"methods": {}
}
]
}Arquitetura Gerada
Infraestrutura Global (gerada pelo init)
src/
├── modules/
│ └── api/
│ ├── api-client.ts ← Instâncias Axios com interceptors JWT + unwrap de response.data
│ └── api.types.ts ← Tipos de erro: BaseError, ForbiddenError, NotFoundError, ApiError
├── types/
│ ├── global.types.ts ← BasePayload, BaseSuccessResponse, BaseErrorResponse
│ └── shared.types.ts ← Tipos compartilhados (ex: TFieldValidationError)
└── utils/
└── api.utils.ts ← parseApiError e utilitários HTTPPor Módulo (gerado pelo module + add)
src/modules/<module>/
├── contracts/ ← Tipos específicos por operação
│ ├── <fn>.types.ts ← Payload, Response, ErrorMap
│ └── index.ts ← barrel (regenerado automaticamente)
├── hooks/ ← Integração TanStack Query
│ ├── use<Fn>.hook.ts ← useMutation (POST/PUT/DELETE) ou useQuery (GET)
│ └── index.ts ← barrel (regenerado automaticamente)
├── <module>.api.ts ← Objeto API com chamadas Axios tipadas
├── <module>.service.ts ← Camada de serviço com try/catch e parseApiError
├── <module>.types.ts ← Interface base do modelo (ex: User, Product)
└── index.ts ← barrel raiz (regenerado automaticamente)Fluxo de Camadas
Componente React
│
▼
hooks/useGetUser.hook.ts → useQuery({ queryFn: ... })
│
▼
users.service.ts → try/catch + parseApiError
│
▼
users.api.ts → api.get<GetUserResponse>(...)
│
▼
api-client.ts → Axios com Bearer token automático
│
▼
Backend REST APIExemplo de Uso no Componente
Após rodar forge add users getUser --get, você pode usar diretamente no seu componente:
import { useGetUser } from '@/modules/users/hooks/useGetUser.hook';
export function UserProfile({ id }: { id: string }) {
const { data, isPending, isError } = useGetUser();
if (isPending) return <Spinner />;
if (isError) return <ErrorMessage />;
return <div>{data.user.name}</div>;
}O TypeScript inferirá automaticamente o tipo de data como GetUserResponse.
Roadmap
- [x] Fase 1 — Nova interface CLI (V2 commands, aliases, modo interativo)
- [x] Fase 2 — Generators próprios (lógica extraída para
generator/) - [x] Fase 3 — State engine melhorado (sync V2, tipos consolidados)
- [x] Fase 4 —
forge liste suporte multi-backend (forge client) - [x]
forge describe - [x]
forge remove - [x]
forge rename - [x] Sync detectar clients órfãos/missing no
api-client.ts - [x] Barrel exports automáticos (
index.ts) - [ ] Barrel exports automáticos (
index.ts) - [ ] Suporte a WebSocket (
forge ws ...)
Licença
ISC © vitongovisk
