Typed Zig Client Library for consuming ServiceStack APIs.
- Typed Request/Response DTOs, generated from any ServiceStack API
- Response Types inferred from the Request DTO at comptime
- Structured
ResponseStatuserrors with field validation errors - Auth with Basic Auth, API Keys, JWT Bearer Tokens and Session Cookies
- Batched Requests, one-way Requests and custom URLs
- Zero dependencies, only the Zig standard library
Requires Zig 0.15+.
zig fetch --save https://github.com/ServiceStack/servicestack-zig/archive/refs/tags/v0.1.3.tar.gzThen add the module to your build.zig:
const servicestack = b.dependency("servicestack", .{ .target = target, .optimize = optimize });
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "servicestack", .module = servicestack.module("servicestack") }},
}),
});Generate the Zig DTOs of any ServiceStack API with the get-dtos tool:
npx get-dtos zig https://blazor-vue.web-templates.ioWhich downloads a dtos.zig containing the typed DTOs of the remote API:
const ss = @import("servicestack");
// @Route("/hello/{Name}")
pub const Hello = struct {
pub const ss_name = "Hello";
pub const ss_verb = "GET";
pub const Response = HelloResponse;
name: ?[]const u8 = null,
};
pub const HelloResponse = struct {
result: ?[]const u8 = null,
responseStatus: ?ss.ResponseStatus = null,
};The generated ss_name, ss_verb and Response declarations are what let the
client resolve each API's route, HTTP Method and Response Type at comptime.
const std = @import("std");
const ss = @import("servicestack");
const dtos = @import("dtos.zig");
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var client = try ss.JsonServiceClient.init(allocator, "https://blazor-vue.web-templates.io");
defer client.deinit();
var res = try client.send(dtos.Hello{ .name = "World" }); // res.value is a HelloResponse
defer res.deinit();
std.debug.print("{s}\n", .{res.value.result.?});
}Responses are returned as a std.json.Parsed(T) that owns its memory — call
deinit() when you're done with it.
send uses the HTTP Method the API is annotated with, use get, post, put,
patch or delete to send a Request DTO with a specific HTTP Method:
var res = try client.post(dtos.Hello{ .name = "World" });APIs that don't return a Response Body are sent with sendVoid:
try client.sendVoid(dtos.DeleteBooking{ .id = 1 });AutoQuery APIs return a typed ss.QueryResponse(T), with the query params of
their base type flattened into the Request DTO:
var res = try client.send(dtos.QueryBookings{ .take = 5, .orderByDesc = "id" });
defer res.deinit();
for (res.value.results.?) |booking| {
std.debug.print("{d} {s}\n", .{ booking.id, booking.name.? });
}Failed API Requests return error.WebServiceException, with the HTTP Status Code
and structured error available from client.getError():
if (client.send(dtos.CreateBooking{})) |res| {
defer res.deinit();
} else |_| {
const web_ex = client.getError().?;
std.debug.print("{d} {s}: {s}\n", .{
web_ex.status_code, // 400
web_ex.errorCode(), // "NotEmpty"
web_ex.errorMessage(), // "'Name' must not be empty."
});
std.debug.print("{?s}\n", .{web_ex.fieldError("Name")});
std.debug.print("{}\n", .{web_ex.isUnauthorized()}); // false
}Alternatively api returns errors in its result instead of an error union:
const api = try client.api(dtos.CreateBooking{});
defer api.deinit();
if (api.failed()) {
std.debug.print("{s} {?s}\n", .{ api.errorCode(), api.fieldError("Name") });
} else {
std.debug.print("{s}\n", .{api.response.?.id.?});
}API Keys and JWTs are sent in the Bearer Token Authorization header:
client.setBearerToken("ak-87949de37e894627a9f6173154e7cafa");HTTP Basic Auth credentials:
client.setCredentials("username", "password");Sign in with ServiceStack's Authenticate API. The client retains the Session
Cookies the Server returns (std.http.Client has no cookie jar of its own), so
subsequent Requests stay authenticated:
var auth = try client.authenticate("username", "password");
defer auth.deinit();const requests = [_]dtos.Hello{ .{ .name = "A" }, .{ .name = "B" } };
var res = try client.sendAll(dtos.HelloResponse, requests[0..]);
defer res.deinit();Or send a Request to a one-way endpoint that ignores its Response:
try client.publish(dtos.Hello{ .name = "World" });Use postFileWithRequest to upload a file with an API Request:
var res = try client.postFileWithRequest(dtos.UploadPhoto{ .album = "Holiday" }, .{
.field_name = "file",
.file_name = "photo.png",
.content_type = "image/png",
.contents = bytes,
});
defer res.deinit();The Request DTO's populated properties are sent as form fields alongside the
file. To upload multiple files use postFilesWithRequest.
If the Server returns a 401 Unauthorized Response either because the client was
unauthenticated or its Bearer Token or API Key had expired, use the
on_authentication_required callback to re-authenticate before the original
Request is automatically retried:
fn signIn(client: *ss.JsonServiceClient) anyerror!void {
var auth = try client.authenticate("username", "password");
auth.deinit();
}
client.on_authentication_required = signIn;
// Automatically retries Requests returning 401 Responses
var res = try client.send(dtos.Secured{});
defer res.deinit();A configured Refresh Token takes precedence over the callback:
client.setRefreshToken(refresh_token);var res = try client.getUrl(dtos.HelloResponse, "/hello/World");
defer res.deinit();
const csv = try client.sendUrlString(.GET, "/api/QueryBookings.csv", null);
defer allocator.free(csv);try client.setHeader("X-Custom", "Value");
try client.setBasePath(""); // use the /json/reply pre-defined routes
client.cookies.clear(); // clear the Session CookiesJsonServiceClient.init sends Requests to ServiceStack's pre-defined /api
route. Use setBasePath("") for older ServiceStack instances that only have the
/json/reply routes enabled.
- examples/hello.zig — typed APIs, batched Requests, validation errors and authentication
zig build examplezig build test # unit tests
zig build test-integration # integration tests against test.servicestack.netReleases are cut with npm scripts and published by the release GitHub Action:
npm run bump # 0.1.0 -> 0.1.1 (also `-- minor`, `-- major`, `-- 1.2.3`)
# describe the release in CHANGELOG.md, then
npm run releaseOr in a single step:
npm run release -- patchnpm run release tags the version, pushes it and creates the GitHub Release,
which triggers the workflow that runs the tests and verifies the release tarball can be fetched.
MIT. See LICENSE.