Call it from React
The same contract generates a typed client for your frontend. Here a React app lists and creates notes through TanStack Query hooks. You need the notes API from Your first API, running on port 3000. The client reads only the contract folder, so the server behind it can be any of the eleven. The commands use npx for Node: see commands per stack. For Vue, Angular or Expo, follow the same steps with Vue, Angular or Expo.
1. Create the React app
Section titled “1. Create the React app”Next to notes-app, create a Vite app and install seampact in it.
npm create vite@latest notes-web -- --template react-tscd notes-webnpm installnpm install @tanstack/react-querynpm install --save-dev @seampact/cli @seampact/codegen-ts @seampact/codegen-fetch @seampact/codegen-reactEach app has its own seampact.config.yaml. This one reads the contract of the backend and generates three targets. Create it in notes-web.
outDir: src/__generated__sources: notes: path: ../notes-app/contracttargets: - id: types use: "@seampact/codegen-ts" apis: [notes] - id: client use: "@seampact/codegen-fetch" apis: [notes] link: { types: types } - id: queries use: "@seampact/codegen-react" apis: [notes] options: { query: tanstack } link: { client: client }clientis the typedfetchclient. Its functions come from the fetch client.querieswrites the React hooks over that client.query: tanstackpicks TanStack Query; SWR and RTK Query work too, see React.
npx seampact generateYou see one line per target, and the files appear in src/__generated__.
2. Reach the backend
Section titled “2. Reach the backend”The browser calls localhost:5173 and the API lives on localhost:3000. Let Vite forward the contract’s paths, so the browser sees one origin and needs no CORS setup. Replace vite.config.ts.
import react from "@vitejs/plugin-react";import { defineConfig } from "vite";
export default defineConfig({ plugins: [react()], server: { proxy: { "/v1": "http://localhost:3000" } },});For a deployed app, see Point the client at your backend.
3. Use the hooks
Section titled “3. Use the hooks”Replace src/App.tsx.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";import { useState } from "react";import * as NotesClient from "./__generated__/client/notes";import { createNotesHooks } from "./__generated__/queries/notes";
const client = NotesClient.createClient({ baseUrl: "" });const Notes = createNotesHooks(client);const queryClient = new QueryClient();
function NoteList() { const [title, setTitle] = useState(""); const notes = Notes.useListNotes({}); const createNote = Notes.useCreateNote();
return ( <div> <input value={title} onChange={(event) => setTitle(event.target.value)} /> <button onClick={() => createNote.mutate({ body: { title } })}>Add</button> <ul> {notes.data?.body.items.map((note) => ( <li key={note.noteId}>{note.title}</li> ))} </ul> </div> );}
export default function App() { return ( <QueryClientProvider client={queryClient}> <NoteList /> </QueryClientProvider> );}createClienttakes the backend URL. An empty one keeps the paths relative, so the Vite proxy answers.createNotesHooks(client)gives one hook per operation:useListNotes,useCreateNote,useGetNoteanduseDeleteNote.- After
createNote, the hooks refresh the list on their own.
Start the app and open the address it prints.
npm run devYou see an empty list. Type a title and click Add: the note appears. The server refuses an empty title with a 400, and the hook’s error holds it.
Without hooks
Section titled “Without hooks”The client alone gives you each reply as data, with no library.
const reply = await client.listNotes({});
if (reply.ok) console.log(reply.status, reply.body.items);Errors, errorStyle and idempotencyKey are in the fetch client.
Next: check it in CI, so the generated files in both apps never drift from the contract.