Skip to content

zio-blocks-endpoint: Pure endpoint descriptor module #1119

@987Nabil

Description

@987Nabil

Motivation

ZIO HTTP's Endpoint type currently mixes pure endpoint description (what method, what path, what query params, what the body looks like, what the response shape is) with runtime en/decoding concerns (EncoderDecoder, HttpContentCodec, StringSchemaCodec, CodecConfig). This makes the endpoint type non-portable — it can't be used without pulling in the full zio-http runtime.

We propose a new zio-blocks-endpoint module containing the pure data types that describe an endpoint's shape. The runtime layer (actual serialization, handler binding, request/response construction) stays in zio-http and interprets these descriptors.

This is the companion to #1113 (zio-blocks-http), which provides the pure HTTP data model (Request, Response, Headers, etc.). This module builds on top of it to describe typed endpoints.

Dependencies

zio-blocks-http           (Method, Status, Path, MediaType, ...)  — #1113
zio-blocks-schema         (Schema[A], Reflect, Doc)
zio-blocks-combinators    (Combiner, Alternator — PR #948)
zio-blocks-mediatype      (MediaType)

zio-blocks-endpoint       (depends on all above)

Design Principles

  • Same names as zio-httpEndpoint, PathCodec, HttpCodec, AuthType, not EndpointDescriptor etc. The blocks types ARE the canonical types. zio.blocks.endpoint.Endpoint is clear from the package.
  • No functions in the dataTransformOrFail is excluded for now (contains opaque A => Either[String, B]). May be added back later in a blocks-style implementation.
  • No Annotated wrapper nodes — follow the blocks Reflect pattern where doc, examples, default are direct fields on each node, not wrapping AST nodes.
  • No encode/decode logic — pure description only. The runtime interpreter lives in zio-http.
  • No ZIO dependency

Components

# Type Purpose Key Design Decisions
1 Endpoint Top-level endpoint description route: RoutePattern[PathInput], input: HttpCodec[Input], output: HttpCodec[Output], error: HttpCodec[Err], auth: AuthType, doc: Doc. No implement* methods, no Invocation, no codec errors.
2 RoutePattern[A] Method + path pattern method: Method, pathCodec: PathCodec[A]. No -> handler binding.
3 PathCodec[A] Path matching pattern AST Sealed trait: Segment, Concat, Annotated. No mutable _optimize cache — pure AST only, compilation to fast matcher is done by the runtime. No TransformOrFail. No Fallback (rarely used, adds complexity).
4 SegmentCodec[A] Single path segment descriptor Empty, Literal(value), IntSeg(name), LongSeg(name), StringSeg(name), UUIDSeg(name), BoolSeg(name), Trailing. Pure names + types, no decoding logic.
5 HttpCodec[A] Composable typed descriptor for request/response parts Sealed trait AST with combinator nodes: Empty, Combine(left, right, combiner), Fallback(left, right, alternator). Atom types listed below. No TransformOrFail. No Annotated wrapper — metadata is on the atoms directly.
6 QueryCodec[A] Query parameter descriptor (atom) name: String, schema: Schema[A], default: Option[A], doc: Doc, examples: Map[String, A], deprecated: Option[Doc].
7 HeaderCodec[A] Header descriptor (atom) name: String, schema: Schema[A], default: Option[A], doc: Doc, examples: Map[String, A], deprecated: Option[Doc].
8 BodyCodec[A] Body content descriptor (atom) schema: Schema[A], mediaTypes: Set[MediaType], name: Option[String], doc: Doc, examples: Map[String, A], deprecated: Option[Doc]. Describes what, not how to encode.
9 StatusCodec[A] Status code descriptor (atom) Specified(status: Status) or Unspecified. With doc, examples fields.
10 AuthType Authentication scheme descriptor Sealed trait: None, Basic, Bearer, Digest, Custom(headerName: String, schema: Schema[A]), Or(left, right), Scoped(inner, scopes: List[String]). Pure description, no codec wiring.
11 Combiner[L, R] Tuple composition From PR #948 — not defined here, just used.
12 Alternator[L, R] Sum composition From PR #948EitherAlternator / UnionAlternator. Not defined here, just used.
13 Doc Documentation From zio-blocks-schema — not defined here, just used.

Metadata Design: Fields, Not Wrappers

Following the pattern established by zio.blocks.schema.Reflect, where doc, examples, and defaultValue are direct fields on every node (not wrapping Annotated AST nodes):

zio-http (wrapper pattern) zio-blocks-endpoint (field pattern)
HttpCodec.Annotated(codec, Metadata.Documented(doc)) QueryCodec("name", schema, doc = Doc.p("the name"))
HttpCodec.Annotated(codec, Metadata.Examples(map)) QueryCodec("name", schema, examples = Map("ex1" -> "John"))
HttpCodec.Annotated(codec, Metadata.Named(name)) Already covered by atom's name field
HttpCodec.Annotated(codec, Metadata.Optional()) default: Option[A] on the atom — absence is OK when default is present. For Schema[Option[A]], absence naturally produces None. No separate flag needed.
HttpCodec.Annotated(codec, Metadata.Deprecated(doc)) deprecated: Option[Doc] field on atom

Benefits:

  • No AST depth just for metadata — cleaner, faster traversal
  • Self-describing — default values are inspectable (useful for OpenAPI default: field)
  • Consistent with how Reflect.Record, Reflect.Variant, etc. already work in zio-blocks-schema

Open Design Question: HttpCodec AST Shape

The combinator nodes (Empty, Combine, Fallback) are shared between request and response codecs, but the atom types differ:

Request atoms: QueryCodec, HeaderCodec, BodyCodec, MethodCodec
Response atoms: StatusCodec, HeaderCodec, BodyCodec

Three options under consideration:

Option Shape Trade-off
A: Two separate ASTs RequestCodec[A] and ResponseCodec[A], each with own atom types Clean type separation. Duplicates combinator nodes (Combine, Fallback).
B: One generic AST HttpCodec[Atoms, A] parameterized by atom kind No duplication. More abstract — atoms discriminated by phantom/union type parameter.
C: Unified AST with tags Single HttpCodec[AtomTypes, A] with phantom type tags (like zio-http today) Proven pattern. Tags leak complexity into API.

Input welcome on which approach fits best with the blocks design philosophy.

What Stays in zio-http

Component Why It Stays
EncoderDecoder Interprets descriptor AST into actual request/response encode/decode
HttpContentCodec[A] Maps MediaType → BinaryCodec[A] — actual serialization
StringSchemaCodec Runtime query/header string codec
Endpoint.implement* Binds handler to endpoint — produces Route
EndpointExecutor / EndpointClient Runtime execution machinery
CodecConfig Runtime codec configuration
Invocation Runtime: pairs endpoint with actual input values
OpenAPI generation Reads the descriptor AST, produces OpenAPI spec
PathCodec _optimize / compiled matcher Runtime optimization compiled from the pure PathCodec AST

Non-Goals

  • No encode/decode logic — pure description only
  • No ZIO dependency — no effects anywhere
  • No TransformOrFail — excluded for now. Functions in data make it non-inspectable/non-serializable. May add back later in a blocks-idiomatic way.
  • No PathCodec.Fallback — rarely used, adds AST complexity
  • No OpenAPI generation — stays in zio-http (or a separate module)
  • No server/client runtime — no Handler, Route, Server, Client

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions