Type Safety

tapik’s type safety matters because the same typed contract drives authoring, generation, and runtime interpretation.

The point is not abstract purity. The point is that tapik can keep generated code aligned with your endpoint definitions without guessing from strings.

What tapik keeps in the type system

While you build an endpoint, tapik preserves:

  • the path and query parameters that appear in the URI,

  • whether query parameters are required or optional,

  • request headers and request body shape,

  • the set of possible outputs,

  • the body and header shape attached to each output branch.

That shape survives long enough for generators to use it directly.

Why that matters to users

Because tapik retains the contract structure precisely:

  • optional query parameters can become optional generated method parameters,

  • required query parameters stay required,

  • response branches can become a sealed response hierarchy,

  • typed headers and bodies can surface as typed properties instead of raw maps.

This is what keeps client and server generation specific instead of generic.

A practical example

If you declare:

val getProduct by endpoint(
    description = "Get a product by ID"
) {
    get("api" / "v1" / "products" / path.string("productId") + query.string("locale").optional("en-US"))
        .input(header.uuid("X-Request-Id"))
        .output(Status.Ok) { jsonBody<ProductView>("product") }
        .output(Status.NotFound) { jsonBody<ProblemDetails>("problem") }
}

tapik still knows all of this later:

  • productId is part of the path and must be supplied,

  • locale is a query parameter with a default,

  • X-Request-Id is a required header,

  • the endpoint has two distinct response branches.

The resulting value is not just "some endpoint". Its type still carries that structure:

HttpEndpoint<
    Parameters2<String, String>,
    Input<Headers1<UUID>, EmptyBody>,
    Outputs2<
        Output<Headers0, JsonBody<ProductView>>,
        Output<Headers0, JsonBody<ProblemDetails>>
    >
>

That is why a generated client can expose a method with a defaulted locale argument, and why a generated server interface can mirror the same input contract.

How tapik does it

Internally, tapik uses typed tuples such as Parameters, Headers, Input, and Outputs to hold the structure of the contract as you compose it.

Most users never need to work with those types directly. What matters is the behavior they enable:

  • ordering is preserved,

  • required versus optional inputs are preserved,

  • multi-output endpoints do not collapse into one weakly typed response.

What this does not guarantee

tapik does not eliminate runtime HTTP problems. A remote service can still return invalid data, the wrong content type, or an unexpected status.

What tapik does guarantee is narrower and more useful:

  • the contract you authored is explicit,

  • generators work from that contract instead of inference,

  • changing the contract forces corresponding generated code to change with it.