Summary#
A client block is a typed wrapper around an external HTTP API. You declare the base URL once and then, for each
endpoint, an operation tagged with its HTTP verb and path. Calling the operation makes the request and returns
the typed result — you never build a URL or parse a response by hand. It is the declarative counterpart to the
imperative Http.* facade: reach for a client when you call the same API repeatedly.
Signature#
client <Name> {
BaseUrl = "<https://…>"; // required: the API's root
// optional: Timeout, Auth, Retry, Headers …
[Get("/path/{param}")] // the verb + path; {param} binds to a same-named argument
<Result> <Op>(<params>);
[Post("/path"), ResponsePath("data")] // ResponsePath unwraps a nested JSON field
<Result> <Op>(<Body> body, [Query] string q, [Header] string h);
}Description#
Naming the verb and path — [Get("/users/{id}")]#
Each operation carries exactly one verb attribute naming its method and path: [Get], [Post], [Put],
[Patch], or [Delete] — e.g. [Get("/users/{id}")]. A {name} segment in the path is a path parameter:
it binds to the operation argument of the same name, so [Get("/track/{code}")] fills {code} from the code
argument.
Where does each argument go — query, header, body?#
An argument that is not a path parameter is bound by an attribute on it:
[Query]binds the argument to a query-string value:[Query] string pageTokenbecomes?pageToken=….[Header]binds it to a request header.- An un-attributed argument on a
[Post]/[Put]/[Patch]is the request body — serialized as JSON.
Unwrapping the response with [ResponsePath]#
Many APIs wrap the payload you want in an envelope — { "data": { … } } or { "messages": [ … ] }. [ResponsePath]
names the field to unwrap, so the operation returns just that part already typed: [Get("/messages"), ResponsePath("messages")] Message[] List(); hands you the array, not the envelope.
Timeout, auth, retry and headers for the whole block#
Beyond BaseUrl, a client may set a Timeout, an Auth (e.g. an API key drawn from a Secret),
a Retry policy, and default Headers sent on every request. These are declared once at the top of the block and
apply to every operation.
You just call an operation — there is no async and no await (async / await — why Osy# has neither). Like any effect, the
platform makes the request in place and resumes your function with the result; the suspension is the engine's business,
not something the signature or the call site has to spell.
Generating a client from an OpenAPI spec#
You rarely hand-write a client for a large API — generate it. osy import-api <spec> reads an OpenAPI/Swagger
document (a local file, a URL, or a GitHub blob URL) and writes an .osy file containing the whole client block: one
operation per endpoint — already tagged with its verb, path, and [Query]/[Header]/[ResponsePath] bindings — plus
the request/response class types and any enums, and an Auth block wired to a Secret. Import
just the operations you need with --tag, --filter, or --select; name the auth secret with --secret and its
method with --auth (bearer, apiKey-header, apiKey-query). The output is ordinary source — review it, trim it,
and commit it like any other client.
osy import-api ./resend-openapi.yaml --tag Emails --auth bearer --secret ResendApiKey -o resend.osy
# → resend.osy: `client Resend { BaseUrl = "…"; Auth = new BearerAuth { Secret = Secret.ResendApiKey }; … }`
# plus the request/response classes. It reminds you to declare the secret once in your app:
# app.Secrets = [ new Secret("ResendApiKey") ]; // value injected out-of-band, never committedUse --list to see the available operations and tags before importing, and --json to preview the source it would
generate without writing a file.
Examples#
class TrackResult {
string Status;
string Location;
}
client Tracking {
BaseUrl = "https://api.tracking.example";
// GET /track/{code}?carrier=… with an API key header; unwrap the "data" envelope.
[Get("/track/{code}"), ResponsePath("data")]
TrackResult Track(string code, [Query] string carrier, [Header] string apiKey);
}class ShipmentRequest {
string OrderCode;
string Address;
}
class ShipmentResult {
string TrackingNumber;
}
client Shipping {
BaseUrl = "https://api.ship.example";
// The un-attributed `body` argument is the JSON request body.
[Post("/shipments"), ResponsePath("shipment")]
ShipmentResult CreateShipment(ShipmentRequest body);
}See also#
- Http.* — the imperative
Http.*facade, for a one-off call rather than a reused API - declaring secrets (app.Secrets) — where a client's API key comes from (
Secret.Name) - publishing a REST API (app.Apis) — the other direction: publishing your app as a REST API
- async / await — why Osy# has neither — why there is no
async; you just call the operation