Current section
Files
Jump to
Current section
Files
README.md

# 🐑 ewe
ewe [/juː/] - fluffy HTTP/1 and HTTP/2 web server for Gleam.
[](https://hex.pm/packages/ewe)
[](https://hexdocs.pm/ewe/)
## Contents
- [Installation](#installation)
- [Usage](#usage)
- [Getting Started](#getting-started)
- [HTTPS](#https)
- [HTTP/2](#http2)
- [Sending a Response](#sending-a-response)
- [Reading the Request Body](#reading-the-request-body)
- [Streaming Bodies](#streaming-bodies)
- [Serving Files](#serving-files)
- [Client Address](#client-address)
- [WebSocket](#websocket)
- [Server-Sent Events](#server-sent-events)
- [Connection Limits and Timeouts](#connection-limits-and-timeouts)
- [Running Under Supervision](#running-under-supervision)
- [Running as an OTP Application](#running-as-an-otp-application)
- [Graceful Shutdown](#graceful-shutdown)
- [Examples](#examples)
- [API Reference](#api-reference)
Most section headings are links, each one opening the runnable example it is
based on.
<h2 id="installation">Installation</h2>
```sh
gleam add ewe@9 gleam_erlang gleam_otp gleam_http logging
```
<h2 id="usage">Usage</h2>
<h3 id="getting-started"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/getting_started.gleam">Getting Started</a></h3>
A handler takes a
[`request.Request(ewe.Connection)`](https://hexdocs.pm/ewe/ewe.html#Connection)
and returns a
[`response.Response(ewe.Body)`](https://hexdocs.pm/ewe/ewe.html#Body). The
request argument the handler receives contains the connection, which you pass to
[`ewe.read_body`](https://hexdocs.pm/ewe/ewe.html#read_body) to read the body,
or to [`ewe.file`](https://hexdocs.pm/ewe/ewe.html#file) and
[`ewe.websocket`](https://hexdocs.pm/ewe/ewe.html#websocket).
To listen on a Unix domain socket instead of a port, use
[`ewe.unix`](https://hexdocs.pm/ewe/ewe.html#unix).
[`ewe.listening_random`](https://hexdocs.pm/ewe/ewe.html#listening_random) lets the
OS pick a free port and [`ewe.start`](https://hexdocs.pm/ewe/ewe.html#start)
returns the address as the actor's started data. Name the server with
[`ewe.named`](https://hexdocs.pm/ewe/ewe.html#named) to look that address later
with [`ewe.get_server_info`](https://hexdocs.pm/ewe/ewe.html#get_server_info).
```gleam
import ewe
import gleam/erlang/process
import gleam/http/request
import gleam/http/response
import logging
pub fn main() {
logging.configure()
logging.set_level(logging.Info)
let assert Ok(_) =
ewe.new(handler: handle_request)
|> ewe.bind(to: "0.0.0.0")
|> ewe.listening(on: 8080)
|> ewe.start
process.sleep_forever()
}
fn handle_request(
_request: request.Request(ewe.Connection),
) -> response.Response(ewe.Body) {
// Give every body a `content-type`. ewe writes `content-length` and
// `transfer-encoding` itself, ypu don't need to specify those headers.
response.new(200)
|> response.set_header("content-type", "text/plain; charset=utf-8")
|> response.set_body(ewe.Text("Hello, World!"))
}
```
<h3 id="https"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/https.gleam">HTTPS</a></h3>
Enable TLS with [`ewe.with_tls`](https://hexdocs.pm/ewe/ewe.html#with_tls), which
takes the certificate source as a [`ewe.Tls`](https://hexdocs.pm/ewe/ewe.html#Tls)
value. The certificate and key are checked when the server starts and
`ewe.start` returns an error if they are missing or invalid.
```gleam
ewe.new(handler: handle_request)
|> ewe.bind(to: "0.0.0.0")
|> ewe.listening(on: 8080)
// Certificate and key files on disk.
|> ewe.with_tls(ewe.Disk("priv/localhost.crt", "priv/localhost.key"))
// Or PEM already in memory: ewe.Pem(cert, key)
// Or DER in memory: ewe.Der(cert, key, ewe.RsaPrivateKey)
|> ewe.start
```
To refuse clients that do not present a certificate signed by an authority you
name, add [`ewe.with_client_verification`](https://hexdocs.pm/ewe/ewe.html#with_client_verification).
It needs TLS to be configured.
```gleam
|> ewe.with_tls(ewe.Disk("priv/localhost.crt", "priv/localhost.key"))
|> ewe.with_client_verification(ewe.CaCertFile("priv/ca.crt"))
```
<h3 id="http2">HTTP/2</h3>
HTTP/2 is always enabled. Over TLS it is offered through ALPN, and a client that
does not choose `h2` is served HTTP/1.1. A cleartext connection is served as
HTTP/2 when it starts with the HTTP/2 preface.
<h3 id="sending-a-response"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/sending_response.gleam">Sending a Response</a></h3>
A response body is one of the [`ewe.Body`](https://hexdocs.pm/ewe/ewe.html#Body)
variants. `Text`, `Bytes` and `Empty` are built by hand, the rest come from
[`ewe.file`](https://hexdocs.pm/ewe/ewe.html#file),
[`ewe.stream_response`](https://hexdocs.pm/ewe/ewe.html#stream_response),
[`ewe.sse`](https://hexdocs.pm/ewe/ewe.html#sse) and
[`ewe.websocket`](https://hexdocs.pm/ewe/ewe.html#websocket).
```gleam
import ewe
import gleam/bytes_tree
import gleam/crypto
import gleam/http/request
import gleam/http/response
import gleam/int
import gleam/result
fn handle_request(
request: request.Request(ewe.Connection),
) -> response.Response(ewe.Body) {
case request.path_segments(request) {
["hello", name] -> {
// Text for text responses.
response.new(200)
|> response.set_header("content-type", "text/plain; charset=utf-8")
|> response.set_body(ewe.Text("Hello, " <> name <> "!"))
}
["bytes", amount] -> {
// Bytes for binary responses built from a `BytesTree`.
let body =
int.parse(amount)
|> result.unwrap(0)
|> crypto.strong_random_bytes
|> bytes_tree.from_bit_array
|> ewe.Bytes
response.new(200)
|> response.set_header("content-type", "application/octet-stream")
|> response.set_body(body)
}
_segments ->
// Empty for responses with no body like 404 or 204.
response.new(404)
|> response.set_body(ewe.Empty)
}
}
```
<h3 id="reading-the-request-body"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/reading_body.gleam">Reading the Request Body</a></h3>
[`ewe.read_body`](https://hexdocs.pm/ewe/ewe.html#read_body) reads the whole body
into memory and fails with `ewe.BodyTooLarge` if it is over `limit` bytes.
Trailer fields sent after the body are added to the returned request's headers.
```gleam
fn handle_request(
request: request.Request(ewe.Connection),
) -> response.Response(ewe.Body) {
let content_type =
request.get_header(request, "content-type")
|> result.unwrap("application/octet-stream")
case ewe.read_body(request, limit: 10_240) {
Ok(req) ->
response.new(200)
|> response.set_header("content-type", content_type)
|> response.set_body(ewe.Bytes(bytes_tree.from_bit_array(req.body)))
Error(ewe.BodyTooLarge) ->
response.new(413)
|> response.set_header("content-type", "text/plain; charset=utf-8")
|> response.set_body(ewe.Text("Body too large"))
Error(ewe.InvalidBody) ->
response.new(400)
|> response.set_header("content-type", "text/plain; charset=utf-8")
|> response.set_body(ewe.Text("Invalid request"))
}
}
```
On HTTP/1, a body the handler did not read is read and discarded after the
response, so the connection can be reused. A body over `auto_drain_limit` causes
the connection to be closed instead.
<h3 id="streaming-bodies"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/streaming_bodies.gleam">Streaming Bodies</a></h3>
[`ewe.read_body_chunk`](https://hexdocs.pm/ewe/ewe.html#read_body_chunk) reads
the body a piece at a time, at most `max_chunk_bytes` per call, instead of all
at once. Each [`ewe.Chunk`](https://hexdocs.pm/ewe/ewe.html#ReadEvent) comes with
the request to pass to the next call.
To send a body in pieces use
[`ewe.stream_response`](https://hexdocs.pm/ewe/ewe.html#stream_response).
Its function gets an
[`ewe.ResponseWriter`](https://hexdocs.pm/ewe/ewe.html#ResponseWriter),
writes with [`ewe.send_chunk`](https://hexdocs.pm/ewe/ewe.html#send_chunk) and
must end the response with
[`ewe.finish_chunk`](https://hexdocs.pm/ewe/ewe.html#finish_chunk) or
[`ewe.finish_response`](https://hexdocs.pm/ewe/ewe.html#finish_response). It
runs in the process serving the request.
```gleam
fn handle_stream(
req: request.Request(ewe.Connection),
max_chunk_bytes: Int,
) -> response.Response(ewe.Body) {
let content_type =
request.get_header(req, "content-type")
|> result.unwrap("application/octet-stream")
response.new(200)
|> response.set_header("content-type", content_type)
|> ewe.stream_response(echo_body(req, _, max_chunk_bytes))
}
// Read the request body one chunk at a time and write each one back out.
//
fn echo_body(
req: request.Request(ewe.Connection),
writer: ewe.ResponseWriter,
max_chunk_bytes: Int,
) -> Result(Nil, ewe.SendError) {
case ewe.read_body_chunk(req, max_chunk_bytes:, limit: 10_485_760) {
Ok(ewe.Chunk(data:, request:)) -> {
use writer <- result.try(ewe.send_chunk(writer, data))
echo_body(request, writer, max_chunk_bytes)
}
Ok(ewe.Done(_request)) -> ewe.finish_response(writer)
Error(_body_error) -> ewe.finish_response(writer)
}
}
```
<h3 id="serving-files"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/serving_files.gleam">Serving Files</a></h3>
[`ewe.file`](https://hexdocs.pm/ewe/ewe.html#file) prepares a file as a response
body. Pass the request's body first since how the file is sent depends on the
connection. `offset` and `limit` send only part of the file.
```gleam
case ewe.file(request.body, resolved, offset: None, limit: None) {
Ok(file) ->
response.new(200)
|> response.set_header("content-type", "application/octet-stream")
|> response.set_body(file)
Error(_error) -> not_found()
}
```
<h3 id="client-address"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/client_info.gleam">Client Address</a></h3>
[`ewe.get_client_info`](https://hexdocs.pm/ewe/ewe.html#get_client_info) returns
the address the request came from as an
[`ewe.SocketAddress`](https://hexdocs.pm/ewe/ewe.html#SocketAddress). The
address is read once when the client connects.
```gleam
fn describe_client(connection: ewe.Connection) -> String {
case ewe.get_client_info(connection) {
ewe.TcpSocketAddress(ip_address:, port:) -> {
let host = case ip_address {
ewe.IpV6(..) -> "[" <> ewe.ip_address_to_string(ip_address) <> "]"
ewe.IpV4(..) -> ewe.ip_address_to_string(ip_address)
}
host <> ":" <> int.to_string(port)
}
ewe.UnixSocketAddress(path: "") -> "unix socket"
ewe.UnixSocketAddress(path:) -> "unix:" <> path
}
}
```
Behind a proxy this is the proxy's address rather than the browser's. The one the
proxy puts in `x-forwarded-for` is the address to use there. MDN's
[security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#security_and_privacy_concerns)
is worth a read before you rely on it for anything since an address taken on
trust is an address anyone can choose.
<h3 id="websocket"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/websocket.gleam">WebSocket</a></h3>
[`ewe.websocket`](https://hexdocs.pm/ewe/ewe.html#websocket) turns a request into
a WebSocket. A request that is not a valid handshake is answered with a 400 and
your handler never runs. Frames from the client and messages from the rest of
your program arrive as [`ewe.WebsocketMessage`](https://hexdocs.pm/ewe/ewe.html#WebsocketMessage)
values. Answer them with [`ewe.send_text_frame`](https://hexdocs.pm/ewe/ewe.html#send_text_frame)
or [`ewe.send_binary_frame`](https://hexdocs.pm/ewe/ewe.html#send_binary_frame)
and say what happens next with
[`ewe.Next`](https://hexdocs.pm/ewe/ewe.html#Next).
On HTTP/1 the request is the usual `Upgrade: websocket` handshake and on HTTP/2
it is the extended `CONNECT` of
[RFC 8441](https://www.rfc-editor.org/rfc/rfc8441) which ewe advertises with
`SETTINGS_ENABLE_CONNECT_PROTOCOL`. To keep WebSockets on HTTP/1 only, turn it
off:
```gleam
|> ewe.with_http2(ewe.Http2Options(..ewe.default_http2_options(), websocket: False))
```
```gleam
fn handle_topic(
req: request.Request(ewe.Connection),
pubsub: Subject(pubsub.Message(Broadcast)),
topic: String,
) -> response.Response(ewe.Body) {
ewe.websocket(
request: req,
// Called once. The selector is where you add whatever the rest of your
// program sends to this connection.
on_init: fn(_conn, selector) {
let client = process.new_subject()
pubsub.subscribe(pubsub, topic:, client:)
let state = WebsocketState(pubsub:, topic:, client:)
let selector = process.select(selector, client)
#(state, selector)
},
handler: handle_websocket_message,
// Called once however the WebSocket ended.
on_close: fn(state) {
pubsub.unsubscribe(state.pubsub, topic: state.topic, client: state.client)
},
)
}
fn handle_websocket_message(
conn: ewe.WebsocketConnection,
state: WebsocketState,
message: ewe.WebsocketMessage(Broadcast),
) -> ewe.Next(WebsocketState, Broadcast) {
case message {
ewe.TextFrame(text) -> {
pubsub.publish(state.pubsub, topic: state.topic, message: Text(text))
ewe.continue(state)
}
ewe.BinaryFrame(data) -> {
pubsub.publish(state.pubsub, topic: state.topic, message: Bytes(data))
ewe.continue(state)
}
// A message from the rest of the program.
ewe.UserMessage(broadcast) -> {
let sent = case broadcast {
Text(text) -> ewe.send_text_frame(conn, text)
Bytes(data) -> ewe.send_binary_frame(conn, data)
}
case sent {
Ok(Nil) -> ewe.continue(state)
Error(_send_error) ->
ewe.stop_abnormal("Failed to send a frame")
}
}
}
}
```
Ping and pong frames are answered by the server and never reach the handler. To
start the closing handshake yourself, return
[`ewe.send_close_frame`](https://hexdocs.pm/ewe/ewe.html#send_close_frame) with a
[`ewe.CloseReason`](https://hexdocs.pm/ewe/ewe.html#CloseReason). No frame can be
sent after it.
<h3 id="server-sent-events"><a target="_blank" href="https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/sse.gleam">Server-Sent Events</a></h3>
[`ewe.sse`](https://hexdocs.pm/ewe/ewe.html#sse) turns a response into an SSE
stream which runs until the handler stops it or the client disconnects. Like a
WebSocket, `on_init` receives a selector to add whatever the rest of your program
sends to this stream, `handler` is called for each message it picks up and
`on_close` runs once the stream ends. The `content-type` and `cache-control`
headers the stream needs are set by ewe.
```gleam
response.new(200)
|> ewe.sse(
on_init: fn(_conn, selector) {
let client = process.new_subject()
pubsub.subscribe(pubsub, topic:, client:)
#(client, process.select(selector, client))
},
handler: fn(conn, client, message) {
case ewe.send_event(conn, ewe.event(message)) {
Ok(Nil) -> ewe.continue(client)
Error(_send_error) -> ewe.stop()
}
},
on_close: fn(client) {
pubsub.unsubscribe(pubsub, topic:, client:)
},
)
```
An event is built with [`ewe.event`](https://hexdocs.pm/ewe/ewe.html#event) and
can carry a name, an id and a reconnection delay through
[`ewe.event_name`](https://hexdocs.pm/ewe/ewe.html#event_name),
[`ewe.event_id`](https://hexdocs.pm/ewe/ewe.html#event_id) and
[`ewe.event_retry`](https://hexdocs.pm/ewe/ewe.html#event_retry).
[`ewe.comment`](https://hexdocs.pm/ewe/ewe.html#comment) sends something clients
ignore which is the usual way to keep an idle stream from being closed by a
proxy.
<h3 id="connection-limits-and-timeouts">Connection Limits and Timeouts</h3>
Every connection is held to a set of limits and timeouts. Start with
[`ewe.default_http1_options`](https://hexdocs.pm/ewe/ewe.html#default_http1_options)
or [`ewe.default_http2_options`](https://hexdocs.pm/ewe/ewe.html#default_http2_options),
update the fields you care about and hand the result to
[`ewe.with_http1`](https://hexdocs.pm/ewe/ewe.html#with_http1) or
[`ewe.with_http2`](https://hexdocs.pm/ewe/ewe.html#with_http2). Sizes are in bytes
and timeouts in milliseconds.
```gleam
let http1 =
ewe.Http1Options(
..ewe.default_http1_options(),
// Refuse a request carrying more than 50 header fields with a 431.
max_headers: 50,
// Close a connection that sits idle for 30 seconds.
idle_timeout: 30_000,
)
let http2 =
ewe.Http2Options(
..ewe.default_http2_options(),
// Cap how many streams a client may have open at once.
max_concurrent_streams: Some(100),
// Trip a GOAWAY sooner on a client resetting streams in bulk.
rapid_reset_threshold: 50,
)
ewe.new(handler: handle_request)
|> ewe.with_http1(http1)
|> ewe.with_http2(http2)
|> ewe.start
```
[`ewe.Http1Options`](https://hexdocs.pm/ewe/ewe.html#Http1Options):
| Field | Default | What it does |
| --- | --- | --- |
| `max_request_line` | `8192` | Longer request lines are refused with a 414. |
| `max_header_line` | `8192` | Longer header lines are refused with a 431. |
| `max_headers` | `100` | Requests carrying more header fields are refused with a 431. |
| `max_chunk_size_line` | `128` | Longer chunk size lines in a chunked body are refused with a 413. |
| `idle_timeout` | `10_000` | How long a connection may stay idle before it is closed. |
| `body_read_timeout` | `10_000` | How long `read_body` and `read_body_chunk` wait for more of the body before failing. |
| `auto_drain_limit` | `1_048_576` | The largest unread body discarded so the connection can be reused. A larger body closes the connection. |
| `auto_drain_chunk_bytes` | `65_536` | How many bytes are read at a time while an unread body is discarded. |
[`ewe.Http2Options`](https://hexdocs.pm/ewe/ewe.html#Http2Options):
| Field | Default | What it does |
| --- | --- | --- |
| `max_concurrent_streams` | `Some(100)` | How many streams a client may have open at once. |
| `initial_window_size` | `262_144` | How much request body a client may send on a new stream before the server allows more. |
| `max_frame_size` | `16_384` | Largest frame accepted, between 16384 and 16777215. |
| `max_header_list_size` | `Some(32_768)` | Requests with larger decoded headers are answered with a 431. |
| `header_table_size` | `4096` | Size of the HPACK table used to decode request headers. |
| `max_continuation_frames` | `100` | How many CONTINUATION frames one header block may use. |
| `max_header_block_bytes` | `65_536` | Largest header block across its HEADERS and CONTINUATION frames. |
| `rapid_reset_window` | `10_000` | The time over which `rapid_reset_threshold` counts resets. |
| `rapid_reset_threshold` | `100` | Most streams reset while their handler is still running, within the window. More resets close the connection. Guards against Rapid Reset (CVE-2023-44487) and MadeYouReset (CVE-2025-8671). |
| `handshake_timeout` | `10_000` | How long the client has to send its SETTINGS and acknowledge ours. |
| `idle_timeout` | `60_000` | How long a connection may stay idle before it is sent GOAWAY and closed. |
| `recv_window_low_water_mark` | `65_536` | When a client can send only this much more on a stream, the server lets it send more. |
| `recv_window_high_water_mark` | `262_144` | How much request body a stream holds before the handler reads it. |
| `websocket` | `True` | Whether a client may open a WebSocket over HTTP/2 with the extended `CONNECT` of RFC 8441. |
| `send_buffer_limit` | `1_048_576` | How much a streamed body, SSE or WebSocket may queue for a slow client before the next write waits. |
| `file_read_threshold` | `1_048_576` | Files up to this size are read into memory, larger ones are sent from disk. |
| `body_read_timeout` | `10_000` | How long `read_body` and `read_body_chunk` wait for more of the body. |
Each read from the socket takes in at most
[`ewe.buffer_size`](https://hexdocs.pm/ewe/ewe.html#buffer_size) bytes, 64 KiB
by default. A larger size means fewer reads for clients that send large bodies.
<h3 id="running-under-supervision">Running Under Supervision</h3>
[`ewe.start`](https://hexdocs.pm/ewe/ewe.html#start) runs the server on its own.
When it belongs to a supervision tree next to the rest of your program use
[`ewe.supervised`](https://hexdocs.pm/ewe/ewe.html#supervised) instead, which
returns a child specification.
```gleam
supervisor.new(supervisor.OneForAll)
|> supervisor.add(pubsub.worker(pubsub_name))
|> supervisor.add(
ewe.new(handler:)
|> ewe.bind(to: "0.0.0.0")
|> ewe.listening(on: 8080)
|> ewe.supervised,
)
|> supervisor.start
```
The line printed on startup comes from [`ewe.on_start`](https://hexdocs.pm/ewe/ewe.html#on_start),
which receives the scheme and the address the server bound to. Replace it to log
it your own way or silence it with [`ewe.quiet`](https://hexdocs.pm/ewe/ewe.html#quiet).
<h3 id="running-as-an-otp-application">Running as an OTP Application</h3>
The examples start the server straight from `main` with a `let assert`, which is
the shortest thing that works while you are trying ewe out. A service is better
off letting the [OTP application](https://www.erlang.org/doc/apps/kernel/application.html)
controller own the supervision tree: it starts before anything else runs, it
brings the tree down in order on shutdown and it is what a release expects.
Point `application_start_module` at a module exporting `start/2` and `stop/1`:
```toml
[erlang]
application_start_module = "my_app"
```
`start` returns the top supervisor's pid, which the application controller then
watches.
```gleam
import gleam/erlang/atom
import gleam/erlang/process
import gleam/otp/actor
import gleam/otp/static_supervisor as supervisor
/// The Erlang/OTP application start callback. Starts the top supervisor and
/// hands its pid back to the application controller.
pub fn start(_type: a, _args: b) -> Result(process.Pid, actor.StartError) {
case
supervisor.new(supervisor.OneForOne)
|> supervisor.add(
ewe.new(handler: handle_request)
|> ewe.bind(to: "0.0.0.0")
|> ewe.listening(on: 8080)
|> ewe.supervised,
)
|> supervisor.start
{
Ok(actor.Started(pid:, ..)) -> Ok(pid)
Error(reason) -> Error(reason)
}
}
/// The Erlang/OTP application stop callback, called once every process in the
/// tree is down. Any final clean up goes here.
pub fn stop(_state: a) -> atom.Atom {
atom.create("ok")
}
/// The application is already running by the time this is called, so all main
/// has left to do is keep the node alive.
pub fn main() {
process.sleep_forever()
}
```
> [!NOTE]
> `main` still has to sleep. `gleam run` boots the application and then calls it,
> so without it the node exits as soon as it returns.
<h3 id="graceful-shutdown">Graceful Shutdown</h3>
> [!NOTE]
> This only happens when the server is in the supervision tree of an OTP
> application as in [Running as an OTP Application](#running-as-an-otp-application).
> A server started from `main`, even under a supervisor, is killed with the VM
> on SIGTERM.
When OTP stops the server, each connection gets to finish before it is closed.
HTTP/1 connections finish the request they are serving, WebSockets are sent a
close frame with code 1001 (going away), SSE streams end, and HTTP/2 connections
send GOAWAY and wait for their open streams.
[`ewe.shutdown_timeout`](https://hexdocs.pm/ewe/ewe.html#shutdown_timeout) sets
how long that may take, 15 seconds by default.
```gleam
ewe.new(handler: handle_request)
|> ewe.shutdown_timeout(30_000)
|> ewe.supervised
```
<h2 id="examples">Examples</h2>
Most sections above link to a runnable example. They live in [examples](examples/).
<h2 id="api-reference">API Reference</h2>
For detailed API documentation, see [hexdocs.pm/ewe](https://hexdocs.pm/ewe/ewe.html).