Forge generates a TypeScript client from your API specification. The client covers REST, WebSocket and SSE, and it ships with a browser runtime that caches entities rather than responses.
The difference shows up the first time two screens display the same record. With a response-keyed cache, GET /orders/7 and GET /orders hold two unrelated copies of order 7, and keeping them in step is invalidation you write by hand in every frontend that talks to your API. With a normalized cache, both reference one Order:7, and a PATCH that changes its total updates the detail page, the row in the list and a sidebar badge with no refetch and no wiring.
What you write to get it01
Nothing, in the common case. Identity is inferred in Go from your response types: a named struct with an id field becomes a cacheable entity, and every non-GET that touches it is derived to invalidate that entity's collection. There is no annotation to add and no client-side invalidation map to maintain.
// No client-specific options. This is enough.
r.GET("/orders/:id", getOrder,
forge.WithOperationID("getOrder"),
forge.WithResponseSchema(200, "Order", &Order{}),
)// generated
getOrder: {
method: 'GET',
path: '/orders/{id}',
entity: 'Order',
rootType: 'Order',
provides: ['Order:{id}'],
invalidates: [],
responseCodec: 'Order',
},Annotations exist for the cases inference cannot reach — an entity identified by something other than id, a mutation with cross-entity effects, a channel that pushes updates — and are covered under Entities and Invalidation.
The browser runtime packages (@forge-go/client-core and the framework adapters) are not published to npm yet. A generated client that uses hooks declares a dependency on @forge-go/client-core, and npm install fails with a registry 404 until that ships. The REST client in the same generated package has no such dependency and works today. See Installation.
Start here02
An orders API with no client annotations at all, from Go route to typed hook, showing exactly what the generator produces.
Which packages exist, which are published, and what works today without them.
How identity is inferred from a Go type, when inference refuses, and how to declare it yourself.
The derived same-entity rule, cross-entity tags, suppression, and stream bindings.
generate, check, diff and watch — every flag and every exit code.
What lands in the output directory and what each file is for.
The entity store, tags, and the placement escape hatch.
React, Vue and Angular bindings, and what { live: true } does and does not mean.
Why a query did not refetch — the question a missed invalidation cannot answer for itself.
Designed but not built: capability gating, SSR hydration, optimistic writes.
How the pieces fit03
Go route + options
│
├─ OpenAPI (REST operations, entity identity, cache tags)
└─ AsyncAPI (channels, stream bindings)
│
▼
forge client generate
│
▼
types.ts ops.ts hooks.ts rest.ts codecs.ts
│
▼
@forge-go/client-core entity store, tag graph, transports
│
▼
client-react │ client-vue │ client-angularGenerated output contains types and one-line bindings, no logic. A runtime fix is a version bump of @forge-go/client-core, not a regeneration of every repository that consumes your API.