Angular
seampact writes an Angular service per api over the fetch client. The client sends through your app’s HttpClient, so your interceptors, XSRF settings and withFetch() apply. Reads are signals built on Angular’s resource(). There is no query library. Tested with Angular 22. The types come from TypeScript types.
Add the targets
Section titled “Add the targets”targets: - id: types use: "@seampact/codegen-ts" apis: [notes] - id: client use: "@seampact/codegen-fetch" apis: [notes] link: { types: types } - id: queries use: "@seampact/codegen-angular" apis: [notes] options: { query: fetch } link: { client: client }query: fetch is the only value for now. Then run npx seampact generate.
Provide the service
Section titled “Provide the service”import { bootstrapApplication } from "@angular/platform-browser";import { provideHttpClient, withInterceptors } from "@angular/common/http";import { provideNotesApi } from "./__generated__/queries/notes/index.js";import { App } from "./app/app";import { authInterceptor } from "./app/auth.interceptor";import { environment } from "./environments/environment";
bootstrapApplication(App, { providers: [ provideHttpClient(withInterceptors([authInterceptor])), provideNotesApi({ baseUrl: environment.apiUrl }), ],});App, authInterceptor and environment are yours: your root component, an HttpInterceptorFn that adds the token, and your environment file. apiUrl holds your backend URL: see Point the client at your backend. Note that environment.apiUrl is fixed at build time; for a value chosen at container start, read /config.json in provideAppInitializer.
Two security settings:
- XSRF: keep the api on the page’s origin and call
withXsrfConfiguration({ headerName })with the CSRF header your contract declares. - Cookies on a cross-origin api: set
withCredentialsin an interceptor. The client config has nofetchoption here.
Use it in a component
Section titled “Use it in a component”import { Component, inject, input } from "@angular/core";import { NotesApi } from "./__generated__/queries/notes/index.js";
@Component({ selector: "app-note-list", template: "..." })export class NoteList { readonly folderId = input.required<string>(); readonly notes = inject(NotesApi).listNotes(() => ({ query: { folderId: this.folderId() } })); readonly create = inject(NotesApi).createNote({ retry: 1 });
async add() { const { error } = await this.create.mutate({ body: { title: "New note" } });
if (!error) this.notes.refetch(); }}- A read returns
{ data, error, isLoading, refetch }, each a signal butrefetch. - The request is a function: when a signal it reads changes, the read starts again. Return
nullto skip a read. - Create readers in a field initializer. Elsewhere, pass
{ injector }. - No read runs during server rendering. Pass
{ transferId: "notes" }to read on the server and hand the data to the browser. Angular’s server transfer caps a body at 1 MiB, so a larger read fails there.
The other behaviours (cache, mutate, retry, paging, refused options) are shared with React and Vue. On Angular, a read stops when the component or the injector is destroyed.
A declared error is thrown as a ContractError: isContractError(error) tells it apart from a network failure (fields and errorStyle: Fetch client). inject(NotesApi).client is the client itself, and createNotesQueries(client) gives the plain fetchers, for code outside Angular.
The service only reads and writes. Keep shared state in your own store or signals.
Add state: ngrx to the options, and install @ngrx/store and @ngrx/effects 22:
options: { query: fetch, state: ngrx }seampact then writes typed actions and effects. Your reducers, selectors and state stay yours.
bootstrapApplication(App, { providers: [ provideHttpClient(), provideStore({ notes: notesReducer }), provideNotesApi({ baseUrl: environment.apiUrl }), provideNotesEffects(), ],});
store.dispatch(NotesActions.listNotes({ request: { query: { folderId } } }));
export const notesReducer = createReducer( initialNotes, on(NotesActions.listNotesSuccess, (state, { data }) => ({ ...state, items: data.body.items })), on(NotesActions.listNotesFailure, (state, { error }) => ({ ...state, error: error.message })),);- One action group per resource of the contract:
NotesActionsfornotes. Operations with no resource are in<Api>ApiActions. - Each operation has three actions:
listNoteswith therequest,listNotesSuccesswith therequestanddata,listNotesFailurewith therequestand the typederror. - An effect sends each action through the service. Every error ends in the failure action, a
ContractErrorfor a declared error. The effect keeps running. - A new read of the same request aborts the one still running, which ends in its failure action with an
AbortError. Reads of different requests run side by side. - Writes run side by side. Each write action sends its own idempotency key. To retry a write safely, give the key yourself and dispatch again with the same key:
call: { idempotencyKey }. The server then runs it once. - Effects run wherever the app dispatches, the server included. Dispatch reads in the browser (for example in
afterNextRender) to keep them off the server. - An operation that needs a timeout takes
call: { timeoutMs }in its action. state: ngrxneeds theserviceartefact.- A failure action carries the contract error object, so leave NgRx’s
strictActionSerializabilitycheck off (its default).
NgRx Signals
Section titled “NgRx Signals”Add state: ngrx-signals to the options, and install @ngrx/signals 22:
options: { query: fetch, state: ngrx-signals }seampact then writes one Signal Store feature per operation. Put the ones you need in your own store:
export const NotesStore = signalStore( withState({ folderId: "" }), withListNotes(), withCreateNote(),);
bootstrapApplication(App, { providers: [provideHttpClient(), provideNotesApi({ baseUrl: environment.apiUrl }), NotesStore],});
export class NotesPage { readonly store = inject(NotesStore); readonly notes = computed(() => this.store.listNotes().data?.body.items ?? []);
constructor() { void this.store.sendListNotes({ query: { folderId: "inbox" } }); }}- Each feature adds a state slice named after the operation (
listNotes) and a methodsend<Operation>(sendListNotes). - The slice holds the latest call:
status(idle,pending,successorerror), itsrequest, and itsdataor typederror, aContractErrorfor a declared error. send<Operation>never throws. It resolves to the slice its call ended with.- A new read of the same request aborts the one still running, which resolves with an
AbortError. Reads of different requests run side by side; the slice shows the last one you sent. - Reads do nothing on the server, and resolve to the
idleslice. To read there and hand the data to the browser, usewithListNotes({ transferId: "notes" }): the browser’s first read of the same request takes that data and sends nothing. - Writes run side by side. Each one sends its own idempotency key. To retry a write safely, give the key yourself and send again with the same key:
sendCreateNote(request, { idempotencyKey }). The server then runs it once. - When the store is destroyed, reads still running are aborted, and the slices no longer change.
- Your own store members must not reuse a slice or
send<Operation>name. Features of two apis in one store clash when their operations share a name. NgRx only warns in dev mode, and the last one wins. - An operation that needs a timeout takes
{ timeoutMs }as its second argument. state: ngrx-signalsneeds theserviceartefact.
Options
Section titled “Options”| Option | Values | Default |
|---|---|---|
query |
fetch |
required |
errors |
throw, data (the whole reply as data) |
throw |
state |
ngrx (actions and effects), ngrx-signals (Signal Store features) |
none |
artefacts: { service: false } keeps the plain fetchers only.
Limits
Section titled “Limits”generate refuses staleTime, invalidate, infinite and keys.root (no query library) and a client with errorStyle: throw (link.client.errorStyle.unsupported). See Link errors and the client table.
Point the client at your backend, then MSW mocks to test without a backend.