smithy_beam
Hex package for the smithy-beam code generator.
Generates Elixir and Erlang code from Smithy models (client and server).
Mix and rebar3 helper that runs the Smithy CLI 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
- Smithy CLI on
PATH- macOS:
brew tap smithy-lang/tap && brew install smithy-cli - Verify:
smithy --help
- macOS:
- A JVM that can load the published codegen JARs (Java 21+)
- Network access on first build so the CLI can download Maven dependencies
Elixir (Mix)
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:
mix smithy_beam.generate
mix smithy_beam.generate --force
Erlang (rebar3)
{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:
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}.
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:
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}.
-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:
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.
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:
{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:
defmodule BookstoreCatalogImpl do
@behaviour BookstoreCatalogBehaviour
@impl true
def handle_get_book(_ctx, input, _meta) do
{:ok, %BookstoreCatalogTypes.GetBookOutput{title: input.isbn}}
end
end
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):
:ok = BookstoreCatalogServer.init_handlers()
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:
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
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.