Params, queries and headers
Send everything a request carries through the typed client: a path parameter, a query value, a header the route's validator demands, and URLs and paths built from the same type with $url() and $path().
What you'll learn
- Pass path parameters and query values as strings under param and query, typed by the server's validators
- Send a header a route validates under header, one every request carries with hc's headers option, and know what init overrides
- Build an absolute URL with $url() and a path with $path() from the route's type
A request carries more than a body: a parameter in the path, values in the query string, headers on top. The typed client accepts each under its own key, param, query and header, and the type of each comes from the server's validators, so a call that forgets one does not compile. This lesson finishes the cats client: a search by minimum age, a lookup by id, a delete that needs an API key, and two functions that build URLs from the same type instead of by joining strings.
param and query are strings
const res = await client.posts[':id'].$get({
param: { id: '123' },
query: { page: '1' },
});
Both are passed as strings, "even if the underlying value is of a different type", as the guide says: a path segment and a query value are text on the wire, and the place they become numbers is the server's validator. A validator('param', …) that returns { id: number } types c.req.valid('param').id as a number for the handler, and types the client's param as { id: string }; a validator('query', …) types query the same way, each value string | string[] because a key can repeat, ?tag=a&tag=b. The client converts on the way out, String(id), and the validator converts on the way in. A parameter is required by the path itself: client.posts[':id'].$get() without param is Expected 1-2 arguments, but got 0.
Three ways to send a header
A header one route depends on belongs in that route's contract, and validator('header', …) puts it there: the keys the validator returns become the client's required header input, so a call without x-api-key does not compile.
await client.posts[':id'].$delete({ param: { id }, header: { 'x-api-key': key } });
A header every request should carry, Authorization for a middleware, an Accept-Language, goes on the client once:
const client = hc<AppType>(baseUrl, { headers: { Authorization: `Bearer ${token}` } });
and a header for one call that no validator asked for goes in the call's second argument, client.posts.$get({}, { headers: { 'X-Trace': id } }). The three merge: the client's, then the call's, then the validated header input. There is a fourth, init, a raw RequestInit applied last, and it is a trap: init.headers replaces every header the client built, so a client with Authorization set once loses it on a call that passes init: { headers: … }. Use init for what nothing else expresses, credentials: 'include' to send cookies from a browser, and headers for headers.
URLs from the type
Every route on the client has $url() and $path():
client.posts[':id'].$url({ param: { id: '7' } }).href; // https://api.example/posts/7
client.posts.$path({ query: { page: '2' } }); // /posts?page=2
$url() returns a URL and needs an absolute base; hc('/api') makes it throw Invalid URL. $path() returns the path and query as a string and works with any base. Both take the same param and query the request would, so a link built this way cannot point at a route that does not exist or miss a parameter it needs.
Your task
app.ts is given: GET /cats?minAge= with a query validator, GET /cats/:id with a param validator that refuses a non-integer id, and DELETE /cats/:id whose header validator wants x-api-key: cats-secret. The client takes the key as its second argument and holds it for every delete.
search(minAge)gives the cats at least that old,minAgesent as a string.find(id)gives the cat,nullon 404, and throws anApiErrorwhen the id itself is refused.remove(id)sends the key in theheaderinput;trueon 204,falseon 404, anApiErroron 401.urlOf(id)is the absolute URL of one cat,searchPath(minAge)the path of a search, both from the type.
When it fails
Property 'header' is missing in type '{ param: { id: string; }; }'at$delete: the route's validator makes the key part of the call; pass it underheader, besideparam.Type 'number' is not assignable to type 'string'underparamorquery: convert withString(); the wire carries text.is refused with the wrong keyfails withExpected the promise to reject:removereturned instead of throwing; anything but204and404is anApiError.builds URLs and pathssees/cats/2where the full URL was expected:urlOfused$path(), or joined strings by hand;$url().hrefcarries the origin from the base URL.
Remember
paramandqueryare strings on the client and numbers after the validator; a path parameter is always required.validator('header')makes a header part of a route's input;hc(url, { headers })sends one on every request; a call's{ headers }adds to it;init.headersreplaces all of them.$url()for an absoluteURL,$path()for the path and query; both typed from the route.init: { credentials: 'include' }for cookies from a browser;form:for aFormDatabody with aFilein it.
Stuck? Show a hint
search: client.cats.$get({ query: { minAge: String(minAge) } }). find: client.cats[':id'].$get({ param: { id: String(id) } }), then narrow: 200 gives the cat, 404 gives null, anything else is an ApiError. remove: client.cats[':id'].$delete({ param: { id: String(id) }, header: { 'x-api-key': apiKey } }): 204 true, 404 false, else ApiError. urlOf: client.cats[':id'].$url({ param }).href. searchPath: client.cats.$path({ query }).