atproto pds in zig pds.zat.dev
pds atproto
zds docs comptime-endpoint-inventory.md
4.0 kB
Markdown
at main

adopted from the notes repo during the 2026-08-23 curation pass; project design/retro content lives with its project.

Comptime endpoint inventories #

written against zig 0.16.

The API reference in ZDS (our zig PDS) is not reflection in the usual runtime sense. It is a comptime-known data inventory.

In Zig, top-level const arrays of structs are available to any importing module at compile time and runtime. If the fields are string literals, enum values, and slices of string literals, the compiler can type-check the whole inventory without needing a registry object or AST parser.

For ZDS, each endpoint is a struct with fields like:

pub const Endpoint = struct {
    route: Route,
    method: []const u8,
    path: []const u8,
    group: []const u8,
    auth: []const u8,
    summary: []const u8,
    params: []const []const u8 = &.{},
    body: []const []const u8 = &.{},
    response: []const []const u8 = &.{},
    notes: []const u8 = "",
};

Then:

  • routing loops over endpoints
  • docs rendering loops over endpoints
  • OpenAPI export loops over endpoints
  • tests assert that important method/path pairs map to the intended route enum

This is compile-time friendly, but not magic. We are manually declaring a table that is more structured than comments and less ambitious than a full router DSL.

Why not AST parsing #

AST parsing would let documentation be inferred from calls like router.get("/xrpc/...", handler, ...), but it would also bind the docs system to source syntax rather than program structure. That tends to be brittle in Zig because route registration can be hidden behind helper functions, inline for, comptime parameters, or imported modules.

The current inventory is boring, but boring is valuable:

  • the compiler checks field names and route enum values
  • code search finds every route declaration
  • docs do not depend on a separate source parser
  • metadata can grow naturally

How this could get better #

The next improvement is probably not runtime reflection. It is a stronger comptime registration shape.

Possible direction:

const routes = api_reference.routes(.{
    .{ .GET, "/api", .api_docs, .{ .group = "zds", .summary = "..." } },
    .{ .POST, "/xrpc/com.atproto.repo.createRecord", .repo_create_record, .{
        .group = "repo",
        .auth = "bearer",
        .lexicon = "com.atproto.repo.createRecord",
    } },
});

The helper could validate at comptime that:

  • methods are from a known method enum
  • XRPC paths contain valid NSIDs
  • route enum values are unique where required
  • HEAD and GET pairings are intentional
  • lexicon IDs match path NSIDs

Zig tools that matter here:

  • comptime parameters for route DSL helpers
  • inline for over tuple literals or arrays
  • @typeInfo if we later want to inspect handler signatures or schema structs
  • @hasDecl for optional module-provided metadata
  • std.meta helpers for enum and struct introspection

httpz and route metadata #

Karl's httpz has a real router with method trees, path params, groups, route configuration, middleware, and custom dispatch. It does not expose a public documentation registry in the pinned version used by ZDS.

That means ZDS has two reasonable options:

  • keep its own endpoint inventory and dispatch switch, as it does now
  • wrap httpz route registration with a ZDS-owned registration helper that records metadata and registers the route at the same time

The second option is attractive if ZDS later moves from its current route enum switch to direct httpz actions. For now, the explicit inventory is a good middle ground.

HTMX #

HTMX is attractive when the server owns small HTML fragments and the UI needs stateful server interactions: endpoint try-it forms, authenticated request probes, schema expansion, saved examples, or live health checks.

For plain search, group filtering, and clipboard copy, vanilla JavaScript is lighter and keeps the reference page deployable as one rendered HTML document.