Packages
Mix and rebar3 helper that runs the Smithy CLI to generate Elixir and Erlang clients from Smithy models.
Current section
Files
Jump to
Current section
Files
smithy_beam
README.md
README.md
# smithy_beam
Hex package for the [smithy-beam](https://github.com/f34nk/smithy-beam) code generator.
Generates Elixir and Erlang code from [Smithy](https://smithy.io/) models (client and server).
Mix and rebar3 helper that runs the [Smithy CLI](https://smithy.io/2.0/guides/smithy-cli/index.html) to generate Elixir and Erlang code from Smithy models.
The Java codegen plugins are resolved from Maven Central. This Hex package is the BEAM-native compile-time glue.
Generation runs at **compile time** in the consumer project. Compiling `smithy_beam` itself as a dependency does not generate code.
## Prerequisites
1. [Smithy CLI](https://smithy.io/2.0/guides/smithy-cli/cli_installation.html) on `PATH`
- macOS: `brew tap smithy-lang/tap && brew install smithy-cli`
- Verify: `smithy --help`
2. A JVM that can load the published codegen JARs (Java 21+)
3. Network access on first build so the CLI can download Maven dependencies
## Elixir (Mix)
```elixir
def project do
[
compilers: [:smithy_beam] ++ Mix.compilers(),
elixirc_paths: ["lib", "lib/generated"],
smithy_beam: [
models: "model",
output: "lib/generated",
language: :elixir,
kind: :client,
edition: "2026"
],
deps: [
{:smithy_beam, "~> 0.1", runtime: false}
]
]
end
```
Explicit task:
```shell
mix smithy_beam.generate
mix smithy_beam.generate --force
```
## Erlang (rebar3)
```erlang
{project_plugins, [smithy_beam]}.
{provider_hooks, [
{pre, [{compile, {smithy_beam, generate}}]}
]}.
{smithy_beam, [
{models, "model"},
{output, "src/generated"},
{language, erlang},
{kind, client},
{edition, "2026"}
]}.
```
Explicit task:
```shell
rebar3 smithy_beam generate
```
## Configuration reference
| Key | Required | Default | Notes |
| --- | --- | --- | --- |
| `models` | yes (unless `config_file`) | | Model file or directory |
| `output` | no | `lib/generated` / `src/generated` | Destination for generated sources |
| `language` | yes | | `:elixir` or `:erlang` |
| `kind` | yes | | `:client`, `:server`, or `:types` |
| `edition` | yes (auto config) | | Plugin edition, e.g. `"2026"` |
| `service` | no | | Shape id when the model has multiple services |
| `name` | no | | Optional module/file name stem |
| `codegen_version` | no | `0.3.1` | Maven codegen artifact version |
| `maven_deps` | no | `[]` | Extra GAV strings appended after defaults |
| `config_file` | no | | Escape hatch: use an existing `smithy-build.json` |
| `plugin` | with `config_file` | derived | Plugin id when using `config_file` |
Default Maven dependencies always include the matching `codegen-elixir` or `codegen-erlang` coordinate plus common Smithy AWS-related artifacts. Consumer `maven_deps` are appended.
## Versioning
Hex package version (`smithy_beam`) and Maven codegen versions are independent. Override the generator with `codegen_version` when needed.
Keep `@version` in `mix.exs` and `{vsn, ...}` in `src/smithy_beam.app.src` equal when releasing.
## Generated sources and git
Whether `lib/generated` / `src/generated` is committed is a consumer choice.
## Custom HTTP clients
Generated clients dispatch HTTP through a pluggable adapter. By default Elixir uses `RuntimeHttpClient.Req` and Erlang uses `runtime_http_client_httpc` (OTP `httpc`). Override the adapter by setting `http_client` on the client config map passed into generated operations.
The adapter must implement the runtime HTTP client behaviour: one callback, `request/1`, that takes a built client request and returns an HTTP response or an error.
### Elixir
Implement `@behaviour RuntimeHttpClient`. The request is a `%RuntimeHttpClient.Request{}` (`method`, `url`, `headers`, `body`). Return `{:ok, %RuntimeTypes.HttpResponse{}}` or `{:error, reason}`.
```elixir
defmodule MyHttpClient do
@behaviour RuntimeHttpClient
alias RuntimeHttpClient.Request
alias RuntimeTypes.HttpResponse
@impl RuntimeHttpClient
def request(%Request{method: method, url: url, headers: headers, body: body}) do
# call your HTTP stack with method, url, headers, and body
{:ok, %HttpResponse{status: 200, headers: [], body: ""}}
end
end
```
Pass the module in config:
```elixir
config = %{
base_url: "https://api.example",
http_client: MyHttpClient
}
MyServiceClient.get_item(config, input)
```
### Erlang
Implement `-behaviour(runtime_http_client).` Include `runtime_http_client.hrl` and `runtime_types.hrl`. The request is `#http_client_request{}` (`method`, `url`, `headers`, `body`). Return `{ok, #http_response{}}` or `{error, Reason}`.
```erlang
-module(my_http_client).
-behaviour(runtime_http_client).
-include("runtime_types.hrl").
-include("runtime_http_client.hrl").
-export([request/1]).
request(#http_client_request{
method = Method,
url = Url,
headers = Headers,
body = Body
}) ->
%% call your HTTP stack with Method, Url, Headers, and Body
{ok, #http_response{status = 200, headers = [], body = <<>>}}.
```
Pass the module in config:
```erlang
Config = #{
base_url => <<"https://api.example">>,
http_client => my_http_client
},
my_service_client:get_item(Config, Input).
```
Typical uses: swap in Finch, Gun, or another HTTP stack; inject mocks in tests; add tracing or custom TLS settings around the request.
## Integrating a generated server
Server codegen emits a typed service contract and an HTTP router. It does **not** emit Plug, Bandit, Cowboy, or Ranch listeners. You generate with `kind: :server`, implement the behaviour, call `init_handlers/0` at application start, then adapt your HTTP stack to the generated router.
Module and file names come from the Smithy service name. For a service `BookstoreCatalog`, Elixir emits modules such as `BookstoreCatalogServer` and Erlang emits `bookstore_catalog_server.erl`.
Typical generated pieces (names vary by service and protocol):
| Role | Elixir | Erlang |
| --- | --- | --- |
| Types | `*Types` | `*_types.hrl` |
| Behaviour | `*Behaviour` | `*_behaviour.erl` |
| Server dispatcher | `*Server` | `*_server.erl` |
| HTTP router | `*Router` | `*_router.erl` |
| Protocol codec | e.g. `*RestJson1` | e.g. `*_rest_json_1.erl` |
| Runtime HTTP carriers | `RuntimeTypes` | `runtime_types.hrl` |
You write the impl module yourself. The server dispatcher looks for a default impl named `{Service}Impl` / `{service}_impl` that implements the behaviour.
### 1. Generate server code
Elixir (`mix.exs`): same as the client setup above, but `kind: :server`.
```elixir
smithy_beam: [
models: "model",
output: "lib/generated",
language: :elixir,
kind: :server,
edition: "2026"
]
```
Erlang (`rebar.config`): same as the client setup above, but `{kind, server}`. Include generated headers:
```erlang
{erl_opts, [debug_info, {i, "src/generated"}]}.
{src_dirs, [{"src", [{recursive, true}]}]}.
{smithy_beam, [
{models, "model"},
{output, "src/generated"},
{language, erlang},
{kind, server},
{edition, "2026"}
]}.
```
Add JSON deps your codec needs (commonly `jason` on Elixir, `jsone` on Erlang).
### 2. Implement the behaviour
Each operation becomes `handle_<operation>/3` with `(ctx, input, meta)`. Return `{:ok, output}` / `{ok, Output}` or an error.
Elixir:
```elixir
defmodule BookstoreCatalogImpl do
@behaviour BookstoreCatalogBehaviour
@impl true
def handle_get_book(_ctx, input, _meta) do
{:ok, %BookstoreCatalogTypes.GetBookOutput{title: input.isbn}}
end
end
```
Erlang:
```erlang
-module(bookstore_catalog_impl).
-behaviour(bookstore_catalog_behaviour).
-include("bookstore_catalog_types.hrl").
-export([handle_get_book/3]).
handle_get_book(_Ctx, Input, _Meta) ->
{ok, #get_book_output{title = Input#get_book_input.isbn}}.
```
### 3. Register handlers at startup
Call once before serving traffic (application `start/2`, supervision init, or test helper):
```elixir
:ok = BookstoreCatalogServer.init_handlers()
```
```erlang
ok = bookstore_catalog_server:init_handlers().
```
`init_handlers/0` loads callbacks from the default impl into persistent term. Without it, router calls through the server module return `{error, not_implemented}`.
### 4. Wire your HTTP stack to the router
Build a `RuntimeTypes.HttpRequest` / `#http_request{}` from the inbound connection (`method`, `path`, `query`, `headers`, `body`), then dispatch:
```elixir
req = %RuntimeTypes.HttpRequest{
method: method,
path: path,
query: query_map,
headers: headers,
body: body
}
case BookstoreCatalogRouter.dispatch(BookstoreCatalogServer, req) do
{:ok, output} ->
resp = BookstoreCatalogRestJson1.encode_get_book_response(output)
# resp.status, resp.headers, resp.body -> send via Plug / Bandit / cowboy_req
{:error, {:not_found, _method, _path}} ->
# 404
{:error, reason} ->
# map modeled errors with codec encode_*_response helpers when applicable
end
```
```erlang
Req = #http_request{
method = Method,
path = Path,
query = QueryMap,
headers = Headers,
body = Body
},
case bookstore_catalog_router:dispatch(bookstore_catalog_server, Req) of
{ok, Output} ->
Resp = bookstore_catalog_rest_json_1:encode_get_book_response(Output),
%% Resp#http_response.status/headers/body -> cowboy_req:reply/4 or similar
{ok, Resp};
{error, {not_found, _Method, _Path}} ->
{error, not_found};
{error, Reason} ->
{error, Reason}
end.
```
`Router.dispatch/2` decodes the wire request, calls `handle_<op>/3`, and returns the handler result. Encoding the success (or error) payload into an HTTP response is a separate codec call that you perform in the adapter.
You may also pass any module that exports the `handle_<op>/3` functions directly to `dispatch/2` (useful in tests) instead of the generated `*Server` module.