Testing an API with the Sampler
This tutorial takes you from a bare checkout to a suite of typed hooks, using the Space Launch System API from the Thymian demo project.
What You’ll Learn
Section titled “What You’ll Learn”By the end of this tutorial, you’ll be able to:
- Run Thymian against an API with nothing set up
- Read the request Thymian would send for any operation
- Write a hook that authenticates, seeds data and asserts
- Turn on editor autocomplete and a type gate for your hooks
- Keep CI honest when the API description changes
Prerequisites
Section titled “Prerequisites”- Node.js 22+ installed
Step 1: Install the Thymian demo project
Section titled “Step 1: Install the Thymian demo project”-
Clone the demo repository
Terminal window git clone https://github.com/thymianofficial/thymian-demo.gitTerminal window git clone git@github.com:thymianofficial/thymian-demo.gitTerminal window gh repo clone thymianofficial/thymian-demo -
Install dependencies
Terminal window npm install -
Start the API
Terminal window npm run dev -
In another terminal, check you can run Thymian
Terminal window npx thymian --version
Step 2: Run the tests
Section titled “Step 2: Run the tests”There is no setup step. Run:
npx thymian testThymian reads the API description, projects a request for every Transaction in it, sends them, and checks the responses against its rules.
Most cases will be skipped with received 401. The Space Launch System API
requires authentication and nothing has supplied any yet.
Step 3: Look at a request
Section titled “Step 3: Look at a request”Before changing anything, see what Thymian is actually sending. Every Transaction has a Selector — its address:
METHOD path [(requestMediaType)] -> status [(responseMediaType)]npx thymian sampler show 'GET /astronauts -> 200 (application/json)'{ "authorize": true, "cookies": {}, "headers": { "accept": "application/json" }, "method": "get", "origin": "http://localhost:3000", "path": "/astronauts", "pathParameters": {}, "query": { "limit": 0, "offset": 0 }}"authorize": true is Thymian reading the security requirement out of the
description. It means “this request wants credentials” — and no hook has offered
any yet.
Get a Selector slightly wrong and Thymian tells you the ones that exist:
npx thymian sampler show 'GET /astronaut -> 200 (application/json)'No transaction matches the selector "GET /astronaut -> 200 (application/json)".Try this: * Check the path against the loaded API description — no transaction with that path is loaded.Step 4: Write an authorize hook
Section titled “Step 4: Write an authorize hook”Hooks live under .thymian/sampler/hooks/. Create the directory and a file:
mkdir -p .thymian/sampler/hooksimport { authorize } from '@thymian/hooks';
export const credentials = authorize((request) => { const token = Buffer.from('sarah.mitchell@space.org:starlight').toString( 'base64', );
request.headers['authorization'] = `Basic ${token}`;});Run the tests again:
npx thymian testThe 401 skips largely disappear. Two things made that work without any setup:
@thymian/hooksresolves through the plugin itself, so a hook file runs with nothing installed beside it and nothing generated.- The
authorizehook ran only where the description says credentials are wanted.GET /astronauts -> 401— the negative case — deliberately did not get them, so it still returns 401 and still passes.
Step 5: Seed data across endpoints
Section titled “Step 5: Seed data across endpoints”Some Transactions need a resource that exists. GET /astronauts/{id} -> 200
is sent with a generated id that matches nobody.
Create one first, and read it back:
import { beforeEach } from '@thymian/hooks';
export const seedAstronaut = beforeEach( 'GET /astronauts/{id} -> 200 (application/json)', async (request, ctx, utils) => { const created = await utils.request( 'POST /astronauts (application/json) -> 201 (application/json)', { body: { name: 'Seeded ' + utils.randomString(6), email: utils.randomString(8) + '@space.org', password: 'hunter2', role: 'Commander', }, }, // A nested request carries its own target's authorize flag, not the // caller's — so say so explicitly. { authorize: true }, );
if (created.statusCode === 201) { utils.setPathParam('id', created.body.id); } },);utils.request is addressed by Selector, the same strings you have been using,
and by default it runs the target’s own hooks — so the nested POST is
authorized by the hook you wrote in Step 4, exactly as the real run would be.
Step 6: Turn on the type gate
Section titled “Step 6: Turn on the type gate”Everything so far worked with plain strings. Now let the compiler check them:
npx thymian sampler initDirectory.thymian/sampler/
Directorygenerated/
- request-types.d.ts Endpoints, keyed by Selector, plus the spec-derived unions
- hooks-api.d.ts the @thymian/hooks surface, typed against them
Directoryhooks/
- auth.ts
- seed.ts
- tsconfig.json scaffolded once — yours from here on
init also prints one thing only you can do: add .thymian/sampler to the
exclude list of your project’s own tsconfig.json, so the sampler’s tsconfig
is the one that governs your hooks.
Commit generated/. Those files are the record of what your API description
said, and the baseline the drift gate compares against.
Now your editor completes Selectors, and a mistake is a compile error at the line that made it:
import { beforeEach } from '@thymian/hooks';
export const typo = beforeEach({ path: '/astronaut' }, () => {});error TS2345: Argument of type '{ path: "/astronaut"; }' is not assignable toparameter of type 'HookTarget'. Did you mean '"/astronauts"'?Delete that file again before continuing.
Step 7: Cover a whole subtree
Section titled “Step 7: Cover a whole subtree”One hook can target a set of Transactions with a filter, including paths added to the description later:
import { afterEach } from '@thymian/hooks';
export const observeLaunches = afterEach( { path: '/launches/**', not: { statusClass: '4XX' } }, (response, ctx, utils) => { utils.info(`launches subtree responded ${response.statusCode}`); },);* is exactly one path segment and a trailing ** is one or more — so
/launches/** is everything under /launches, not /launches itself.
A glob that matches nothing fails the run and tells you which paths do exist, so a typo cannot quietly switch a hook off.
Step 8: Keep CI honest
Section titled “Step 8: Keep CI honest”Two commands, and one rule.
npx thymian sampler validatevalidate regenerates the type surface in memory, compares it with what you
committed, and type-checks your hooks against your description as it is now:
- everything matches and compiles → silent
- the committed types are behind, but every hook still compiles → a warning
to run
sampler sync - a hook no longer compiles → breaking drift, named at the hook’s own line
npx thymian sampler sync --check--check writes nothing and exits non-zero if regeneration would change the
committed files. Put it in CI.
A description-only edit, and a pure reordering of your document, are not drift — the comparison ignores comments and formatting.
Where to go next
Section titled “Where to go next”- Hooks API Reference — every hook
kind, every filter field, the whole
utilssurface - How to write a Hook
- Migrating from the samples tree