Skip to content

andreashasse/elli_openapi

Repository files navigation

elli_openapi

Library for building type-safe HTTP APIs with automatic OpenAPI documentation generation using Elli and Spectra. This library is not ready for production use, but it wont take long to finish it.

Usage

  1. Add to your rebar.config dependencies:
{deps, [
    {elli_openapi, "~> 0.1.1"}
]}.
  1. Start Elli with elli_openapi_handler as the callback and your routes as arguments to elli_openapi_handler:
%% Define your routes
Routes = [
    {<<"POST">>, <<"/api/users">>, fun user_handler:create_user/4},
    {<<"GET">>, <<"/api/users/{userId}">>, fun user_handler:get_user/4}
],

%% Configure and start Elli, preferably in your supervisor spec.
ElliOpts = [
    {callback, elli_openapi_handler},
    {callback_args, Routes},
    {port, 3000}
],

{ok, Pid} = elli:start_link(ElliOpts).

You can optionally pass custom OpenAPI metadata by wrapping callback_args in a {MetaData, Routes} tuple:

MetaData = #{title => <<"My API">>, version => <<"1.0.0">>},
ElliOpts = [
    {callback, elli_openapi_handler},
    {callback_args, {MetaData, Routes}},
    {port, 3000}
].

See the example/ directory for a runnable example application with handler implementations.

Handler Functions

All handler functions must follow this signature:

handler_name(PathArgs, QueryArgs, Headers, Body) -> {StatusCode, ResponseHeaders, ResponseBody}

Arguments

  1. PathArgs (map()): URL path parameters extracted from the route

    • Example: For route <<"/api/users/{userId}">>, PathArgs would be #{userId => <<"42">>}
    • Empty map #{} if no path parameters
  2. QueryArgs (map()): URL query parameters

    • Example: #{page => 1, per_page => 20}
    • Declare expected query params in the function spec; undeclared params are ignored
  3. Headers (map()): HTTP request headers with atom keys

    • Example: #{'Authorization' => <<"Bearer ...">>, 'Content-Type' => <<"application/json">>}
    • Required headers must be declared in the function spec
  4. Body (any()): Request body, automatically decoded based on the type in your function spec

    • JSON requests: map() or record type
    • Plain text requests: binary()
    • Bodyless methods (GET, HEAD, etc.): declare as binary() — an empty body decodes cleanly to <<"">>
    • The library validates and decodes the body according to your spec

Return Value

Must be a 3-tuple: {StatusCode, ResponseHeaders, ResponseBody}

  • StatusCode: HTTP status code integer (200, 201, 400, etc.)
  • ResponseHeaders: Map with atom keys (e.g., #{'Location' => <<"...">>, 'ETag' => <<"...">>})
  • ResponseBody: Response body (record, map, or binary) - will be encoded based on content type

To return different status codes from the same handler, use union types in your function spec where each branch represents a possible response:

-spec my_handler(PathArgs, QueryArgs, Headers, Body) ->
    {200, Headers1, SuccessBody}
    | {400, Headers2, ErrorBody}
    | {404, Headers3, NotFoundBody}.

Spec placement

-spectra() metadata attributes and -spec declarations must appear before any function clause in the file. The Erlang compiler processes attributes in declaration order — placing them after a function clause will cause them to be ignored or crash at startup.

%% Correct order
-spectra(#{summary => <<"Create user">>}).
-spec create_user(#{}, #{}, #{}, #user{}) -> {201, #{}, #user{}}.
create_user(#{}, #{}, #{}, User) -> ...

%% Wrong — attributes after a function clause are not processed
some_other_function() -> ...
-spectra(#{summary => <<"Create user">>}).   %% too late
-spec create_user(...) -> ...
create_user(...) -> ...

Handler specs use Spectra's type system. See the Spectra documentation for supported types and serialization rules.

For complete handler examples, see example/src/elli_openapi_demo.erl.

How Routing Works

Routes are compiled into ETS match specifications at startup. Each path template like <<"/api/users/{userId}">> is translated into a match spec pattern where {userId} becomes a match variable. At request time, the incoming path is converted to a tuple of segments and dispatched with a single ets:match_spec_run/2 call, which simultaneously selects the correct handler and extracts all path variable bindings — using the VM's native pattern-matching engine rather than iterating over a list of routes.

Example Application

The example/ directory contains a runnable demo application showcasing multiple handler implementations including user management, echo, status updates, and item updates with conflict detection.

To run the example:

make demo

The demo starts on port 3000. Access the API documentation at:

About

Demo erldantic using elli

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors