Building Robust API Payloads in Gleam with Typed Structs and JSON
When exposing APIs in Gleam, typed structs combined with the gleam/json library give compile‑time guarantees and runtime safety. This architecture note walks through the minimal design, trust boundaries, operational checks, and failure modes that shape a dependable payload layer.
07 Sept 2025, 08:39 UTC

Problem & Takeaway
API designers in Gleam often wrestle with two competing goals: making contracts explicit at compile time while still handling unpredictable external payloads at runtime. Typed structs give you the former; gleam/json gives you the latter. The key takeaway is that a small, well‑defined struct definition, coupled with explicit decode‑error handling, forms a trustworthy boundary between your service and the world.
Requirements
- Gleam 0.33+ (current stable)
- gleam/json 1.1+ (matches your Gleam version)
- An HTTP server (e.g.,
gleam_http,httpd, or an Elixir Phoenix endpoint calling into Gleam) - Unit test framework (built‑in Gleam test)
Minimal Design: A Typed Struct for a User Payload
Define a struct that mirrors the expected JSON shape. Use @derive(json) to auto‑implement encode and decode.
import gleam/json
@derive(json)
pub struct UserPayload {
pub name: Text
pub age: Int
pub email: Text
}
By default, gleam/json converts struct field names to snake_case JSON keys. If your API uses camelCase, add @json(name: "userName") annotations per field.
Trust & Data Boundaries
- External input is untrusted. Never assume the JSON matches the struct; always wrap decoding in a
Resultand handle errors. - Internal state is trusted. Once a struct is decoded successfully, you can treat it as a reliable contract inside your application.
- Keep the struct definition in a dedicated module (e.g.,
payloads/user_payload.gleam) to isolate the contract from business logic.
Operational Checks
1. Compile‑time Guarantees
- Missing fields in the struct cause compile errors, ensuring every required value is represented.
- Type mismatches (e.g.,
age: "twenty") are caught at compile time if you ever write a constructor manually.
2. Runtime Decoding Validation
pub fn handle_request(body: Text) : Result(Text, Text) {
case UserPayload.decode(body) {
Ok(payload) -> {
// Payload is now a strongly‑typed struct
// Business logic here
Ok("Success")
}
Error(err) -> {
// err is of type JsonError, containing details
Error("Invalid payload: " ++ err.message())
}
}
}
Run unit tests that feed malformed JSON (missing keys, wrong types) to confirm that the Error branch is exercised.
3. Size & Performance
- For large payloads, consider streaming decoding if
gleam/jsonsupports it. Otherwise, benchmark withgleam benchto ensure memory usage stays within limits. - Because BEAM is garbage‑collected, large temporary structures can cause pauses. Keep structs lean.
Failure Modes & Design Changes
1. Missing Required Fields
When a client omits a required key, decode returns Error(<JsonError>). If you return a generic 400 error, you lose diagnostics. Instead, surface the JsonError message in the HTTP response or log it for debugging.
2. Type Mismatch
Supplying a string where an integer is expected triggers a decode error. If your API evolves to accept stringified numbers, add a new struct variant or use a custom decoder that normalizes the input.
3. Default Values
Gleam does not automatically apply default values during decoding. If a field is optional, declare it as Maybe(Text). The caller decides whether to supply a value. This explicitness prevents silent bugs that could arise from implicit defaults.
4. Version Drift
If gleam/json releases a breaking change, your compiled BEAM bytecode may behave differently. Pin the dependency version in gleam.toml and run gleam deps pull before each release cycle.
5. Interoperability with Elixir/Erlang
Because Gleam compiles to BEAM, you can import the struct into an Elixir module and call UserPayload.encode(struct) directly. However, remember that Elixir’s Jason library uses snake_case by default; ensure key naming conventions match.
Practical Verification Checklist
- Run
gleam test– ensure all tests pass, including those that intentionally fail decoding. - Execute
gleam runwith a sample JSON payload and observe the decoded struct. - Use
gleam beamto inspect the generated BEAM module; confirm the struct is a tuple with the expected arity. - Benchmark decoding with
gleam benchor a simple timing harness; record throughput and memory usage. - Deploy to a staging environment; send malformed requests and verify that your API returns a clear 400 response.
Conclusion
Typed structs in Gleam, combined with gleam/json, provide a clean, type‑safe contract for API payloads. By keeping the struct definition minimal, handling decode errors explicitly, and guarding against common failure modes, you establish a robust trust boundary that simplifies both development and maintenance. The architecture outlined here scales from simple CRUD endpoints to complex services that interoperate with other BEAM languages.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.