···2233# ๐ ewe
4455-ewe [/juห/] - fluffy package for building web servers. Inspired by [mist](https://github.com/rawhat/mist).
55+ewe [/juห/] - fluffy package for building web servers.
6677[](https://hex.pm/packages/ewe)
88[](https://hexdocs.pm/ewe/)
···1313gleam add ewe@2 gleam_erlang gleam_otp gleam_http logging
1414```
15151616-## Quick Start
1616+## Getting Started
17171818```gleam
1919import gleam/erlang/process
···2121import gleam/http/response
22222323import ewe.{type Request, type Response}
2424-2525-fn handler(_req: Request) -> Response {
2626- response.new(200)
2727- |> response.set_header("content-type", "text/plain; charset=utf-8")
2828- |> response.set_body(ewe.TextData("Hello, World!"))
2929-}
30243125pub fn main() {
3226 logging.configure()
···34283529 let assert Ok(_) =
3630 ewe.new(handler)
3737- |> ewe.bind_all()
3131+ |> ewe.bind_all
3832 |> ewe.listening(port: 8080)
3939- |> ewe.start()
3333+ |> ewe.start
40344135 process.sleep_forever()
4236}
3737+3838+fn handler(_req: Request) -> Response {
3939+ response.new(200)
4040+ |> response.set_header("content-type", "text/plain; charset=utf-8")
4141+ |> response.set_body(ewe.TextData("Hello, World!"))
4242+}
4343```
44444545## Usage
46464747-### [Sending Response](test/preview/sending_response.gleam)
4747+### [Sending Response](examples/src/sending_response.gleam)
48484949`ewe` provides several response body types (see [`ewe.ResponseBody`](https://hexdocs.pm/ewe/ewe.html#ResponseBody) type). Request handler must return [`response.Response`](https://hexdocs.pm/gleam_http/gleam/http/response.html#Response) type with [`ewe.ResponseBody`](https://hexdocs.pm/ewe/ewe.html#ResponseBody). You can also use [`ewe.Request`](https://hexdocs.pm/ewe/ewe.html#Request)/[`ewe.Response`](https://hexdocs.pm/ewe/ewe.html#Response) as they are aliases for `request.Request(Connection)`(see [`request.Request`](https://hexdocs.pm/gleam_http/gleam/http/request.html#Request) & [`ewe.Connection`](https://hexdocs.pm/ewe/ewe.html#Connection))/`response.Response(ResponseBody)`.
5050···6161fn handler(req: Request(Connection)) -> Response(ResponseBody) {
6262 case request.path_segments(req) {
6363 ["hello", name] -> {
6464+ // Use TextData for text responses.
6565+ //
6466 response.new(200)
6567 |> response.set_header("content-type", "text/plain; charset=utf-8")
6668 |> response.set_body(ewe.TextData("Hello, " <> name <> "!"))
6769 }
6870 ["bytes", amount] -> {
7171+ // Use BitsData for binary responses.
7272+ //
6973 let random_bytes =
7074 int.parse(amount)
7175 |> result.unwrap(0)
···7680 |> response.set_body(ewe.BitsData(random_bytes))
7781 }
7882 _ ->
8383+ // Use Empty for responses with no body (like 404, 204, etc).
8484+ //
7985 response.new(404)
8086 |> response.set_body(ewe.Empty)
8187 }
8288}
8389```
84908585-### [Getting Request Body](test/preview/getting_request_body.gleam)
9191+### [Reading Body](examples/src/reading_body.gleam)
86928793To read the body of a request, use [`ewe.read_body`](https://hexdocs.pm/ewe/ewe.html#read_body). This function is intended for cases where the entire body can safely be loaded into memory.
8894···939994100import ewe.{type Request, type Response}
951019696-fn handle_echo(req: Request) -> Response {
102102+fn handler(req: Request) -> Response {
97103 let content_type =
98104 request.get_header(req, "content-type")
99105 |> result.unwrap("application/octet-stream")
100106101101- case ewe.read_body(req, 1024) {
107107+ // Read the entire request body into memory with a 10KB limit. This blocks
108108+ // until the full body is received.
109109+ //
110110+ case ewe.read_body(req, 10_240) {
102111 Ok(req) ->
103112 response.new(200)
104113 |> response.set_header("content-type", content_type)
···115124}
116125```
117126118118-### [Streaming](test/preview/streaming.gleam)
127127+### [Streaming Body](examples/src/streaming_body.gleam)
119128120129For larger request bodies, [`ewe.stream_body`](https://hexdocs.pm/ewe/ewe.html#stream_body) provides a streaming interface. It produces a [`ewe.Consumer`](https://hexdocs.pm/ewe/ewe.html#Consumer) which can be called repeatedly to read fixed-size chunks. This enables efficient handling of large payloads without buffering them fully.
121130···129138 BodyError(ewe.BodyError)
130139}
131140141141+// Recursively consume chunks from the request body and send them to the
142142+// chunked response handler via the subject.
143143+//
132144fn stream_resource(
133145 consumer: ewe.Consumer,
134146 subject: Subject(Message),
135147 chunk_size: Int,
136148) -> Nil {
137137- // Simulating delay for demonstration purpose
138149 process.sleep(int.random(250))
150150+ // Call the consumer with the chunk size. It returns the next chunk of data
151151+ // and a new consumer for the remaining body.
152152+ //
139153 case consumer(chunk_size) {
140154 Ok(ewe.Consumed(data, next)) -> {
141155 logging.log(logging.Info, {
···146160 })
147161148162 process.send(subject, Chunk(data))
163163+ // Recursively process the next chunk.
164164+ //
149165 stream_resource(next, subject, chunk_size)
150166 }
151151- Ok(ewe.Done) -> {
152152- process.send(subject, Done)
153153- }
154154- Error(body_error) -> {
155155- process.send(subject, BodyError(body_error))
156156- }
167167+ Ok(ewe.Done) -> process.send(subject, Done)
168168+ Error(body_error) -> process.send(subject, BodyError(body_error))
157169 }
158170}
159171···162174 request.get_header(req, "content-type")
163175 |> result.unwrap("application/octet-stream")
164176177177+ // Get a consumer function for streaming the request body.
178178+ //
165179 case ewe.stream_body(req) {
166180 Ok(consumer) -> {
181181+ // Set up a chunked response. The response is sent in chunks as we
182182+ // consume the request body.
183183+ //
167184 ewe.chunked_body(
168185 req,
169186 response.new(200) |> response.set_header("content-type", content_type),
187187+ // Spawn a separate process to consume the body and send chunks.
188188+ // This prevents blocking the handler while reading data.
189189+ //
170190 on_init: fn(subject) {
171171- process.spawn(fn() { stream_resource(consumer, subject, chunk_size) })
172172-173173- Nil
191191+ let _pid =
192192+ fn() { stream_resource(consumer, subject, chunk_size) }
193193+ |> process.spawn
174194 },
175195 handler: fn(chunked_body, state, message) {
176196 case message {
···197217}
198218```
199219200200-### [File Serving](test/preview/file_serving.gleam)
220220+### [Serving Files](examples/src/serving_files.gleam)
201221202222Static files can be sent using [`ewe.file`](https://hexdocs.pm/ewe/ewe.html#file). It accepts a path and optional `offset`/`limit` parameters. This allows serving HTML pages, assets, or binary files with minimal effort.
203223···208228import ewe.{type Response}
209229210230fn serve_file(path: String) -> Response {
231231+ // Load file from disk using ewe.file(). This efficiently streams the file
232232+ // content without loading it entirely into memory.
233233+ //
234234+ // In production, make sure to validate paths to prevent directory traversal
235235+ // attacks! (e.g., requests to "../../../etc/passwd")
236236+ //
211237 case ewe.file("public" <> path, offset: None, limit: None) {
212238 Ok(file) -> {
213239 response.new(200)
···223249}
224250```
225251226226-## [WebSocket](test/preview/websocket.gleam)
252252+### [WebSocket](examples/src/websocket.gleam)
227253228254Use [`ewe.upgrade_websocket`](https://hexdocs.pm/ewe/ewe.html#upgrade_websocket) to switch an HTTP request into a WebSocket connection. Incoming messages are represented as [`ewe.WebsocketMessage`](https://hexdocs.pm/ewe/ewe.html#WebsocketMessage). Outgoing frames are sent 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). Handlers control the connection lifecycle with [`ewe.WebsocketNext`](https://hexdocs.pm/ewe/ewe.html#WebsocketNext).
229255···257283258284fn handler(req: Request, pubsub: Subject(PubSubMessage)) -> Response {
259285 case request.path_segments(req) {
260260- ["topic", topic] ->
261261- ewe.upgrade_websocket(
262262- req,
263263- on_init: fn(_conn, selector) {
264264- logging.log(
265265- logging.Info,
266266- "WebSocket connection opened: " <> pid_to_string(process.self()),
267267- )
268268-269269- let client = process.new_subject()
270270- process.send(pubsub, Subscribe(topic:, client:))
271271-272272- let state = WebsocketState(pubsub:, topic:, client:)
273273- let selector = process.select(selector, client)
274274-275275- #(state, selector)
276276- },
277277- handler: handle_websocket,
278278- on_close: fn(_conn, state) {
279279- let assert Ok(pid) = process.subject_owner(state.client)
280280- logging.log(
281281- logging.Info,
282282- "WebSocket connection closed: " <> pid_to_string(pid),
283283- )
284284-285285- process.send(pubsub, Unsubscribe(state.topic, state.client))
286286- },
287287- )
286286+ ["topic", topic] -> handle_topic(req, pubsub, topic)
288287 _ ->
289288 response.new(404)
290289 |> response.set_body(ewe.Empty)
291290 }
292291}
293292294294-fn handle_websocket(
293293+fn handle_topic(req: Request, pubsub: Subject(PubSubMessage), topic: String) {
294294+ // Upgrade the HTTP connection to WebSocket. Unlike SSE, WebSocket is
295295+ // bidirectional - both client and server can send messages at any time.
296296+ //
297297+ ewe.upgrade_websocket(
298298+ req,
299299+ // Initialize the WebSocket connection. The selector allows receiving
300300+ // messages from both the WebSocket and the pubsub system.
301301+ //
302302+ on_init: fn(_conn, selector) {
303303+ let client = process.new_subject()
304304+ process.send(pubsub, Subscribe(topic:, client:))
305305+306306+ let state = WebsocketState(pubsub:, topic:, client:)
307307+ // Add the client subject to the selector to receive broadcast messages.
308308+ //
309309+ let selector = process.select(selector, client)
310310+311311+ #(state, selector)
312312+ },
313313+ handler: handle_websocket_message,
314314+ on_close: fn(_conn, state) {
315315+ process.send(pubsub, Unsubscribe(state.topic, state.client))
316316+ },
317317+ )
318318+}
319319+320320+// Handle three types of messages: text from client, binary from client,
321321+// and broadcast messages from the pubsub system.
322322+//
323323+fn handle_websocket_message(
295324 conn: ewe.WebsocketConnection,
296325 state: WebsocketState,
297326 msg: ewe.WebsocketMessage(Broadcast),
298327) -> ewe.WebsocketNext(WebsocketState, Broadcast) {
299328 case msg {
329329+ // Text message from the client - broadcast to all subscribers.
330330+ //
300331 ewe.Text(text) -> {
301332 process.send(state.pubsub, Publish(state.topic, Text(text)))
302333 ewe.websocket_continue(state)
303334 }
304335336336+ // Binary message from the client - broadcast to all subscribers.
337337+ //
305338 ewe.Binary(binary) -> {
306339 process.send(state.pubsub, Publish(state.topic, Bytes(binary)))
307340 ewe.websocket_continue(state)
308341 }
309342343343+ // User message from the pubsub - forward to this client.
344344+ //
310345 ewe.User(message) -> {
311346 let assert Ok(_) = case message {
312347 Text(text) -> ewe.send_text_frame(conn, text)
···317352 }
318353 }
319354}
320320-321321-fn pid_to_string(pid: Pid) -> String {
322322- charlist.to_string(pid_to_list(pid))
323323-}
324324-325325-@external(erlang, "erlang", "pid_to_list")
326326-fn pid_to_list(pid: Pid) -> Charlist
327355```
328356329329-### [Server-Sent Events](test/preview/sse.gleam)
357357+### [Server-Sent Events](examples/src/sse.gleam)
330358331359332360Use [`ewe.sse`](https://hexdocs.pm/ewe/ewe.html#sse) to establish a Server-Sent Events connection for real-time data streaming to clients. The connection is managed through [`ewe.SSEConnection`](https://hexdocs.pm/ewe/ewe.html#SSEConnection) and events are sent with [`ewe.send_event`](https://hexdocs.pm/ewe/ewe.html#send_event). Handlers control the connection lifecycle with [`ewe.SSENext`](https://hexdocs.pm/ewe/ewe.html#SSENext). This enables efficient one-way communication for live updates, notifications, or real-time data feeds.
333361334362```gleam
335363import gleam/bit_array
336336-import gleam/erlang/charlist.{type Charlist}
337337-import gleam/erlang/process.{type Pid, type Subject}
338338-import gleam/http/request
364364+import gleam/erlang/process.{type Subject}
365365+import gleam/http
339366import gleam/http/response
340340-import logging
341367342342-import ewe.{type Request, type Response}
368368+import ewe
343369344370type PubSubMessage {
345371 Subscribe(client: Subject(String))
···347373 Publish(String)
348374}
349375350350-fn handler(req: Request, pubsub: Subject(PubSubMessage)) -> Response {
351351- case request.path_segments(req) {
352352- ["sse"] ->
376376+fn handler(req: ewe.Request, pubsub: Subject(PubSubMessage)) -> ewe.Response {
377377+ case req.method, req.path {
378378+ http.Get, "/sse" ->
379379+ // Establish a Server-Sent Events connection. SSE is a one-way channel
380380+ // from server to client. The connection stays open and the server can
381381+ // push events at any time.
382382+ //
353383 ewe.sse(
354384 req,
385385+ // Initialize the connection and subscribe this client to the pubsub.
386386+ //
355387 on_init: fn(client) {
356388 process.send(pubsub, Subscribe(client))
357357- logging.log(
358358- logging.Info,
359359- "SSE connection opened: " <> pid_to_string(process.self()),
360360- )
361389362390 client
363391 },
392392+ // Handle messages from the pubsub and send them as SSE events.
393393+ //
364394 handler: fn(conn, client, message) {
365395 case ewe.send_event(conn, ewe.event(message)) {
366396 Ok(Nil) -> ewe.sse_continue(client)
367397 Error(_) -> ewe.sse_stop()
368398 }
369399 },
400400+ // Clean up when the client disconnects.
401401+ //
370402 on_close: fn(_conn, client) {
371403 process.send(pubsub, Unsubscribe(client))
372372- logging.log(
373373- logging.Info,
374374- "SSE connection closed: " <> pid_to_string(process.self()),
375375- )
376404 },
377405 )
378378- ["publish"] -> {
379379- case ewe.read_body(req, 1024) {
380380- Ok(req) -> {
381381- let assert Ok(text) = bit_array.to_string(req.body)
382382- process.send(pubsub, Publish(text))
383383- response.new(200) |> response.set_body(ewe.Empty)
384384- }
385385- Error(_) -> response.new(400) |> response.set_body(ewe.Empty)
386386- }
387387- }
388388- _ -> response.new(404) |> response.set_body(ewe.Empty)
389389- }
390390-}
391391-392392-fn pid_to_string(pid: Pid) -> String {
393393- charlist.to_string(pid_to_list(pid))
394394-}
395395-396396-@external(erlang, "erlang", "pid_to_list")
397397-fn pid_to_list(pid: Pid) -> Charlist
398398-```
399399-400400-### [Complete Preview](test/preview.gleam)
401401-402402-```gleam
403403-import gleam/bit_array
404404-import gleam/crypto
405405-import gleam/dict
406406-import gleam/erlang/charlist.{type Charlist}
407407-import gleam/erlang/process.{type Name, type Pid, type Subject}
408408-import gleam/http/request
409409-import gleam/http/response
410410-import gleam/int
411411-import gleam/list
412412-import gleam/option.{None, Some}
413413-import gleam/otp/actor
414414-import gleam/otp/static_supervisor as supervisor
415415-import gleam/otp/supervision.{type ChildSpecification}
416416-import gleam/result
417417-import gleam/string
418418-import logging
419419-420420-import ewe.{type Request, type Response}
421421-422422-pub fn main() {
423423- logging.configure()
424424- logging.set_level(logging.Info)
425425-426426- // Create a named subject for the pubsub worker
427427- let pubsub_name = process.new_name("pubsub")
428428- let pubsub = process.named_subject(pubsub_name)
429429-430430- // Configure and start the supervision tree with pubsub worker and the ewe
431431- // server, that listens on port 8080
432432- let assert Ok(_) =
433433- supervisor.new(supervisor.OneForAll)
434434- |> supervisor.add(pubsub_worker(pubsub_name))
435435- |> supervisor.add(
436436- ewe.new(handler(_, pubsub))
437437- |> ewe.bind_all()
438438- |> ewe.listening(port: 8080)
439439- |> ewe.supervised(),
440440- )
441441- |> supervisor.start()
442442-443443- process.sleep_forever()
444444-}
445445-446446-// Define the messages that can be sent to the pubsub worker
447447-type PubSubMessage {
448448- Subscribe(topic: String, client: Subject(Broadcast))
449449- Publish(topic: String, message: Broadcast)
450450- Unsubscribe(topic: String, client: Subject(Broadcast))
451451-}
452452-453453-// Define the messages that could be received by websocket and SSE clients
454454-type Broadcast {
455455- Text(String)
456456- Bytes(BitArray)
457457-}
458458-459459-// Define the state of the websocket connection
460460-type WebsocketState {
461461- WebsocketState(
462462- pubsub: Subject(PubSubMessage),
463463- topic: String,
464464- client: Subject(Broadcast),
465465- )
466466-}
467467-468468-// Main logic of the pubsub worker, that handles the messages and keeps track of
469469-// the clients on topics. Its implementation is not really important
470470-fn pubsub_worker(
471471- named: Name(PubSubMessage),
472472-) -> ChildSpecification(Subject(PubSubMessage)) {
473473- let pubsub =
474474- actor.new(dict.new())
475475- |> actor.on_message(fn(state, msg) {
476476- case msg {
477477- Subscribe(topic:, client:) -> {
478478- let new_state =
479479- dict.upsert(in: state, update: topic, with: fn(clients) {
480480- case clients {
481481- Some(clients) -> [client, ..clients]
482482- None -> {
483483- logging.log(logging.Info, "Creating topic " <> topic)
484484- [client]
485485- }
486486- }
487487- })
488488-489489- let assert Ok(pid) = process.subject_owner(client)
490490- logging.log(
491491- logging.Info,
492492- "Subscribing client " <> pid_to_string(pid) <> " to topic " <> topic,
493493- )
494406495495- actor.continue(new_state)
496496- }
497497- Publish(topic:, message:) -> {
498498- case message {
499499- Text(text) ->
500500- logging.log(
501501- logging.Info,
502502- "Publishing text message `" <> text <> "` to topic " <> topic,
503503- )
504504- Bytes(binary) ->
505505- logging.log(
506506- logging.Info,
507507- "Publishing binary message `"
508508- <> string.inspect(binary)
509509- <> "` to topic "
510510- <> topic,
511511- )
512512- }
513513-514514- case dict.get(state, topic) {
515515- Ok(clients) -> list.each(clients, actor.send(_, message))
516516- Error(_) -> Nil
517517- }
518518-519519- actor.continue(state)
520520- }
521521- Unsubscribe(topic:, client:) -> {
522522- let assert Ok(pid) = process.subject_owner(client)
523523- logging.log(
524524- logging.Info,
525525- "Unsubscribing client "
526526- <> pid_to_string(pid)
527527- <> " from topic "
528528- <> topic,
529529- )
407407+ // Accept messages via POST and broadcast them to all SSE clients.
408408+ //
409409+ http.Post, "/post" -> {
410410+ case ewe.read_body(req, 128) {
411411+ Ok(req) -> {
412412+ case bit_array.to_string(req.body) {
413413+ Ok(message) -> {
414414+ process.send(pubsub, Publish(message))
530415531531- let new_state = case dict.get(state, topic) {
532532- Ok([_]) | Ok([]) -> {
533533- logging.log(logging.Info, "Dropping topic " <> topic)
534534- dict.drop(state, [topic])
416416+ response.new(200) |> response.set_body(ewe.Empty)
535417 }
536536- Ok(clients) -> {
537537- list.filter(clients, fn(c) { c != client })
538538- |> dict.insert(state, topic, _)
539539- }
540540- Error(_) -> state
418418+ Error(Nil) -> response.new(400) |> response.set_body(ewe.Empty)
541419 }
542542-543543- actor.continue(new_state)
544420 }
421421+ Error(_) -> response.new(400) |> response.set_body(ewe.Empty)
545422 }
546546- })
547547- |> actor.named(named)
548548-549549- supervision.worker(fn() {
550550- logging.log(logging.Info, "Starting pubsub worker")
551551- actor.start(pubsub)
552552- })
553553-}
554554-555555-// Main HTTP request handler that routes requests to different endpoints
556556-fn handler(req: Request, pubsub: Subject(PubSubMessage)) -> Response {
557557- case request.path_segments(req) {
558558- // GET /hello/:name - Simple greeting endpoint
559559- ["hello", name] -> {
560560- response.new(200)
561561- |> response.set_header("content-type", "text/plain; charset=utf-8")
562562- |> response.set_body(ewe.TextData("Hello, " <> name <> "!"))
563423 }
564564- // GET /bytes/:amount - Generate random N bytes
565565- ["bytes", amount] -> {
566566- let random_bytes =
567567- int.parse(amount)
568568- |> result.unwrap(0)
569569- |> crypto.strong_random_bytes()
570424571571- response.new(200)
572572- |> response.set_header("content-type", "application/octet-stream")
573573- |> response.set_body(ewe.BitsData(random_bytes))
574574- }
575575-576576- // POST /echo - Echo back the request body
577577- ["echo"] -> handle_echo(req)
578578- // POST /stream/:chunk_size - Stream and echo back the request body in chunks
579579- ["stream", chunk_size] ->
580580- handle_stream(req, int.parse(chunk_size) |> result.unwrap(16))
581581-582582- // GET /file/:path - Serve a file from the public directory
583583- ["file", path] -> serve_file(path)
584584-585585- // POST /topic/:topic/ws - Upgrade to WebSocket connection
586586- ["topic", topic, "ws"] ->
587587- ewe.upgrade_websocket(
588588- req,
589589- on_init: fn(_conn, selector) {
590590- logging.log(
591591- logging.Info,
592592- "WebSocket connection opened: " <> pid_to_string(process.self()),
593593- )
594594-595595- let client = process.new_subject()
596596- process.send(pubsub, Subscribe(topic:, client:))
597597-598598- let state = WebsocketState(pubsub:, topic:, client:)
599599- let selector = process.select(selector, client)
600600-601601- #(state, selector)
602602- },
603603- handler: handle_websocket,
604604- on_close: fn(_conn, state) {
605605- let assert Ok(pid) = process.subject_owner(state.client)
606606- logging.log(
607607- logging.Info,
608608- "WebSocket connection closed: " <> pid_to_string(pid),
609609- )
610610-611611- process.send(pubsub, Unsubscribe(state.topic, state.client))
612612- },
613613- )
614614-615615- // POST /topic/:topic/sse - Switch to Server-Sent Events connection
616616- ["topic", topic, "sse"] ->
617617- ewe.sse(
618618- req,
619619- on_init: fn(client) {
620620- logging.log(
621621- logging.Info,
622622- "SSE connection opened: " <> pid_to_string(process.self()),
623623- )
624624-625625- process.send(pubsub, Subscribe(topic:, client:))
626626- client
627627- },
628628- handler: fn(conn, client, message) {
629629- let assert Ok(_) = case message {
630630- Text(text) -> ewe.send_event(conn, ewe.event(text))
631631- _ -> Ok(Nil)
632632- }
633633-634634- ewe.sse_continue(client)
635635- },
636636- on_close: fn(_conn, client) {
637637- logging.log(
638638- logging.Info,
639639- "SSE connection closed: " <> pid_to_string(process.self()),
640640- )
641641-642642- process.send(pubsub, Unsubscribe(topic:, client:))
643643- },
644644- )
645645-646646- // All other routes return 404
647647- _ ->
648648- response.new(404)
649649- |> response.set_body(ewe.Empty)
650650- }
651651-}
652652-653653-fn handle_echo(req: Request) -> Response {
654654- let content_type =
655655- request.get_header(req, "content-type")
656656- |> result.unwrap("application/octet-stream")
657657-658658- case ewe.read_body(req, 1024) {
659659- Ok(req) ->
660660- response.new(200)
661661- |> response.set_header("content-type", content_type)
662662- |> response.set_body(ewe.BitsData(req.body))
663663- Error(ewe.BodyTooLarge) ->
664664- response.new(413)
665665- |> response.set_header("content-type", "text/plain; charset=utf-8")
666666- |> response.set_body(ewe.TextData("Body too large"))
667667- Error(ewe.InvalidBody) ->
668668- response.new(400)
669669- |> response.set_header("content-type", "text/plain; charset=utf-8")
670670- |> response.set_body(ewe.TextData("Invalid request"))
671671- }
672672-}
673673-674674-pub type StreamMessage {
675675- Chunk(BitArray)
676676- Done
677677- BodyError(ewe.BodyError)
678678-}
679679-680680-fn stream_resource(
681681- consumer: ewe.Consumer,
682682- subject: Subject(StreamMessage),
683683- chunk_size: Int,
684684-) -> Nil {
685685- process.sleep(int.random(250))
686686- case consumer(chunk_size) {
687687- Ok(ewe.Consumed(data, next)) -> {
688688- logging.log(logging.Info, {
689689- "Consumed "
690690- <> int.to_string(bit_array.byte_size(data))
691691- <> " bytes: "
692692- <> string.inspect(data)
693693- })
694694-695695- process.send(subject, Chunk(data))
696696- stream_resource(next, subject, chunk_size)
697697- }
698698- Ok(ewe.Done) -> {
699699- process.send(subject, Done)
700700- }
701701- Error(body_error) -> {
702702- process.send(subject, BodyError(body_error))
703703- }
704704- }
705705-}
706706-707707-fn handle_stream(req: Request, chunk_size: Int) -> Response {
708708- let content_type =
709709- request.get_header(req, "content-type")
710710- |> result.unwrap("application/octet-stream")
711711-712712- case ewe.stream_body(req) {
713713- Ok(consumer) -> {
714714- ewe.chunked_body(
715715- req,
716716- response.new(200) |> response.set_header("content-type", content_type),
717717- on_init: fn(subject) {
718718- process.spawn(fn() { stream_resource(consumer, subject, chunk_size) })
719719-720720- Nil
721721- },
722722- handler: fn(chunked_body, state, message) {
723723- case message {
724724- Chunk(data) ->
725725- case ewe.send_chunk(chunked_body, data) {
726726- Ok(Nil) -> ewe.chunked_continue(state)
727727- Error(_) -> ewe.chunked_stop_abnormal("Failed to send chunk")
728728- }
729729- Done -> ewe.chunked_stop()
730730- BodyError(body_error) ->
731731- ewe.chunked_stop_abnormal(string.inspect(body_error))
732732- }
733733- },
734734- on_close: fn(_conn, _state) {
735735- logging.log(logging.Info, "Stream closed")
736736- },
737737- )
738738- }
739739- Error(_) ->
740740- response.new(400)
741741- |> response.set_header("content-type", "text/plain; charset=utf-8")
742742- |> response.set_body(ewe.TextData("Invalid request"))
743743- }
744744-}
745745-746746-fn serve_file(path: String) -> Response {
747747- case ewe.file("public" <> path, offset: None, limit: None) {
748748- Ok(file) -> {
749749- response.new(200)
750750- |> response.set_header("content-type", "application/octet-stream")
751751- |> response.set_body(file)
752752- }
753753- Error(_) -> {
754754- response.new(404)
755755- |> response.set_header("content-type", "text/plain; charset=utf-8")
756756- |> response.set_body(ewe.TextData("File not found"))
757757- }
758758- }
759759-}
760760-761761-fn handle_websocket(
762762- conn: ewe.WebsocketConnection,
763763- state: WebsocketState,
764764- msg: ewe.WebsocketMessage(Broadcast),
765765-) -> ewe.WebsocketNext(WebsocketState, Broadcast) {
766766- case msg {
767767- ewe.Text(text) -> {
768768- process.send(state.pubsub, Publish(state.topic, Text(text)))
769769- ewe.websocket_continue(state)
770770- }
771771-772772- ewe.Binary(binary) -> {
773773- process.send(state.pubsub, Publish(state.topic, Bytes(binary)))
774774- ewe.websocket_continue(state)
775775- }
776776-777777- ewe.User(message) -> {
778778- let assert Ok(_) = case message {
779779- Text(text) -> ewe.send_text_frame(conn, text)
780780- Bytes(binary) -> ewe.send_binary_frame(conn, binary)
781781- }
782782-783783- ewe.websocket_continue(state)
784784- }
425425+ _, _ -> response.new(404) |> response.set_body(ewe.Empty)
785426 }
786427}
787787-788788-fn pid_to_string(pid: Pid) -> String {
789789- charlist.to_string(pid_to_list(pid))
790790-}
791791-792792-@external(erlang, "erlang", "pid_to_list")
793793-fn pid_to_list(pid: Pid) -> Charlist
794428```
795429796430## API Reference
797431798798-For detailed API documentation, see [hexdocs.pm/ewe](https://hexdocs.pm/ewe/ewe.html).432432+For detailed API documentation, see [hexdocs.pm/ewe](https://hexdocs.pm/ewe/ewe.html).
+25
examples/README.md
···11+# Ewe Examples
22+33+This directory contains practical examples demonstrating various features of ewe.
44+55+To run an example, use the following command:
66+```sh
77+gleam run -m <example name>
88+```
99+1010+For example, to run the getting started example:
1111+```sh
1212+gleam run -m getting_started
1313+```
1414+1515+## Examples
1616+1717+Here is a list of all the examples:
1818+1919+- [getting_started](./src/getting_started.gleam) - Basic HTTP server with "Hello, World!" response
2020+- [sending_response](./src/sending_response.gleam) - Different response body types (text, binary, empty)
2121+- [reading_body](./src/reading_body.gleam) - Reading and echoing request bodies with size limits
2222+- [streaming_body](./src/streaming_body.gleam) - Streaming large request/response bodies in chunks
2323+- [serving_files](./src/serving_files.gleam) - Serving static files from disk
2424+- [websocket](./src/websocket.gleam) - WebSocket connections with topic-based pubsub
2525+- [sse](./src/sse.gleam) - Server-Sent Events for real-time server-to-client updates
+15
examples/gleam.toml
···11+name = "examples"
22+version = "1.0.0"
33+description = "A collection of ewe examples"
44+55+[dependencies]
66+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
77+gleam_erlang = ">= 1.3.0 and < 2.0.0"
88+gleam_otp = ">= 1.2.0 and < 2.0.0"
99+logging = ">= 1.3.0 and < 2.0.0"
1010+ewe = { path = "../" }
1111+gleam_http = ">= 4.3.0 and < 5.0.0"
1212+gleam_crypto = ">= 1.5.1 and < 2.0.0"
1313+1414+[dev-dependencies]
1515+gleeunit = ">= 1.0.0 and < 2.0.0"
+36
examples/src/getting_started.gleam
···11+import ewe.{type Request, type Response}
22+import gleam/erlang/process
33+import gleam/http/response
44+import logging
55+66+pub fn main() {
77+ // This sets the logger to print Info level logs. I recommend using `logging`
88+ // package, unless you prefer other tools.
99+ //
1010+ logging.configure()
1111+ logging.set_level(logging.Info)
1212+1313+ // Start the ewe web server.
1414+ //
1515+ let assert Ok(_) =
1616+ ewe.new(handler)
1717+ |> ewe.bind_all
1818+ |> ewe.listening(port: 8080)
1919+ |> ewe.start
2020+2121+ // Put process into sleep.
2222+ //
2323+ process.sleep_forever()
2424+}
2525+2626+// This is the HTTP request handler.
2727+//
2828+fn handler(_req: Request) -> Response {
2929+ // When sending response with body, it is important to include `content-type`
3030+ // header representing what type your body is. You don't need to specify
3131+ // `content-length`, it is calculated automatically by ewe.
3232+ //
3333+ response.new(200)
3434+ |> response.set_header("content-type", "text/plain; charset=utf-8")
3535+ |> response.set_body(ewe.TextData("Hello, World!"))
3636+}
+58
examples/src/sending_response.gleam
···11+import ewe.{type Connection, type ResponseBody}
22+import gleam/crypto
33+import gleam/erlang/process
44+import gleam/http/request.{type Request}
55+import gleam/http/response.{type Response}
66+import gleam/int
77+import gleam/result
88+import logging
99+1010+pub fn main() {
1111+ logging.configure()
1212+ logging.set_level(logging.Info)
1313+1414+ // A server demonstrating different response body types and path routing.
1515+ //
1616+ let assert Ok(_) =
1717+ ewe.new(handler)
1818+ |> ewe.bind_all
1919+ |> ewe.listening(port: 8080)
2020+ |> ewe.start
2121+2222+ process.sleep_forever()
2323+}
2424+2525+fn handler(req: Request(Connection)) -> Response(ResponseBody) {
2626+ // Pattern match on path segments for cleaner routing.
2727+ // Example: "/hello/alice" becomes ["hello", "alice"]
2828+ //
2929+ case request.path_segments(req) {
3030+ ["hello", name] -> {
3131+ // Here, we will use TextData for text responses.
3232+ //
3333+ response.new(200)
3434+ |> response.set_header("content-type", "text/plain; charset=utf-8")
3535+ |> response.set_body(ewe.TextData("Hello, " <> name <> "!"))
3636+ }
3737+ ["bytes", amount] -> {
3838+ // Use BitsData for binary responses. We generate random bytes
3939+ // to demonstrate sending binary data.
4040+ //
4141+ let body =
4242+ int.parse(amount)
4343+ |> result.unwrap(0)
4444+ |> crypto.strong_random_bytes
4545+ |> ewe.BitsData
4646+4747+ response.new(200)
4848+ |> response.set_header("content-type", "application/octet-stream")
4949+ |> response.set_body(body)
5050+ }
5151+ _ ->
5252+ // Use Empty for responses with no body (like 404, 204, etc).
5353+ // You don't need to set content-type for empty bodies.
5454+ //
5555+ response.new(404)
5656+ |> response.set_body(ewe.Empty)
5757+ }
5858+}
+44
examples/src/serving_files.gleam
···11+import ewe.{type Response}
22+import gleam/erlang/process
33+import gleam/http/response
44+import gleam/option.{None}
55+import logging
66+77+pub fn main() {
88+ logging.configure()
99+ logging.set_level(logging.Info)
1010+1111+ // Start a simple file server that serves files from the "public" directory.
1212+ //
1313+ let assert Ok(_) =
1414+ ewe.new(fn(req) { serve_file(req.path) })
1515+ |> ewe.bind_all
1616+ |> ewe.listening(port: 8080)
1717+ |> ewe.start
1818+1919+ process.sleep_forever()
2020+}
2121+2222+fn serve_file(path: String) -> Response {
2323+ // Load file from disk using ewe.file(). This efficiently streams the file
2424+ // content without loading it entirely into memory.
2525+ //
2626+ // In production, make sure to validate paths to prevent directory traversal
2727+ // attacks! (e.g., requests to "../../../etc/passwd")
2828+ //
2929+ case ewe.file("public" <> path, offset: None, limit: None) {
3030+ Ok(file) -> {
3131+ // Using "application/octet-stream" is safe for any file type, but you
3232+ // may want to specify content-type based on file extension in production.
3333+ //
3434+ response.new(200)
3535+ |> response.set_header("content-type", "application/octet-stream")
3636+ |> response.set_body(file)
3737+ }
3838+ Error(_) -> {
3939+ response.new(404)
4040+ |> response.set_header("content-type", "text/plain; charset=utf-8")
4141+ |> response.set_body(ewe.TextData("File not found"))
4242+ }
4343+ }
4444+}
+243
examples/src/websocket.gleam
···11+import ewe.{type Request, type Response}
22+import gleam/dict
33+import gleam/erlang/charlist.{type Charlist}
44+import gleam/erlang/process.{type Name, type Pid, type Subject}
55+import gleam/http/request
66+import gleam/http/response
77+import gleam/list
88+import gleam/option.{None, Some}
99+import gleam/otp/actor
1010+import gleam/otp/static_supervisor as supervisor
1111+import gleam/otp/supervision.{type ChildSpecification}
1212+import gleam/string
1313+import logging
1414+1515+pub fn main() {
1616+ logging.configure()
1717+ logging.set_level(logging.Info)
1818+1919+ // Create a named pubsub process for topic-based message broadcasting.
2020+ // Multiple clients can subscribe to different topics and receive messages
2121+ // sent to those topics.
2222+ //
2323+ let pubsub_name = process.new_name("pubsub")
2424+ let pubsub = process.named_subject(pubsub_name)
2525+2626+ // Set up supervision for both pubsub and the web server.
2727+ //
2828+ let assert Ok(_) =
2929+ supervisor.new(supervisor.OneForAll)
3030+ |> supervisor.add(pubsub_worker(pubsub_name))
3131+ |> supervisor.add(
3232+ ewe.new(handler(_, pubsub))
3333+ |> ewe.bind_all
3434+ |> ewe.listening(port: 8080)
3535+ |> ewe.supervised,
3636+ )
3737+ |> supervisor.start
3838+3939+ process.sleep_forever()
4040+}
4141+4242+fn handler(req: Request, pubsub: Subject(PubSubMessage)) -> Response {
4343+ case request.path_segments(req) {
4444+ ["topic", topic] -> handle_topic(req, pubsub, topic)
4545+ _ ->
4646+ response.new(404)
4747+ |> response.set_body(ewe.Empty)
4848+ }
4949+}
5050+5151+// Websocket
5252+// -----------------------------------------------------------------------------
5353+5454+type WebsocketState {
5555+ WebsocketState(
5656+ pubsub: Subject(PubSubMessage),
5757+ topic: String,
5858+ client: Subject(Broadcast),
5959+ )
6060+}
6161+6262+type Broadcast {
6363+ Text(String)
6464+ Bytes(BitArray)
6565+}
6666+6767+fn handle_topic(req: Request, pubsub: Subject(PubSubMessage), topic: String) {
6868+ // Upgrade the HTTP connection to WebSocket. Unlike SSE, WebSocket is
6969+ // bidirectional - both client and server can send messages at any time.
7070+ //
7171+ ewe.upgrade_websocket(
7272+ req,
7373+ // Initialize the WebSocket connection. The selector allows receiving
7474+ // messages from both the WebSocket and the pubsub system.
7575+ //
7676+ on_init: fn(_conn, selector) {
7777+ logging.log(
7878+ logging.Info,
7979+ "WebSocket connection opened: " <> pid_to_string(process.self()),
8080+ )
8181+8282+ let client = process.new_subject()
8383+ process.send(pubsub, Subscribe(topic:, client:))
8484+8585+ let state = WebsocketState(pubsub:, topic:, client:)
8686+ // Add the client subject to the selector to receive broadcast messages.
8787+ //
8888+ let selector = process.select(selector, client)
8989+9090+ #(state, selector)
9191+ },
9292+ handler: handle_websocket_message,
9393+ on_close: fn(_conn, state) {
9494+ let assert Ok(pid) = process.subject_owner(state.client)
9595+ logging.log(
9696+ logging.Info,
9797+ "WebSocket connection closed: " <> pid_to_string(pid),
9898+ )
9999+100100+ process.send(pubsub, Unsubscribe(state.topic, state.client))
101101+ },
102102+ )
103103+}
104104+105105+// Handle three types of messages: text from client, binary from client,
106106+// and broadcast messages from the pubsub system.
107107+//
108108+fn handle_websocket_message(
109109+ conn: ewe.WebsocketConnection,
110110+ state: WebsocketState,
111111+ msg: ewe.WebsocketMessage(Broadcast),
112112+) -> ewe.WebsocketNext(WebsocketState, Broadcast) {
113113+ case msg {
114114+ // Text message from the client - broadcast to all subscribers.
115115+ //
116116+ ewe.Text(text) -> {
117117+ process.send(state.pubsub, Publish(state.topic, Text(text)))
118118+ ewe.websocket_continue(state)
119119+ }
120120+121121+ // Binary message from the client - broadcast to all subscribers.
122122+ //
123123+ ewe.Binary(binary) -> {
124124+ process.send(state.pubsub, Publish(state.topic, Bytes(binary)))
125125+ ewe.websocket_continue(state)
126126+ }
127127+128128+ // User message from the pubsub - forward to this client.
129129+ //
130130+ ewe.User(message) -> {
131131+ let assert Ok(_) = case message {
132132+ Text(text) -> ewe.send_text_frame(conn, text)
133133+ Bytes(binary) -> ewe.send_binary_frame(conn, binary)
134134+ }
135135+136136+ ewe.websocket_continue(state)
137137+ }
138138+ }
139139+}
140140+141141+// PubSub
142142+// -----------------------------------------------------------------------------
143143+144144+type PubSubMessage {
145145+ Subscribe(topic: String, client: Subject(Broadcast))
146146+ Publish(topic: String, message: Broadcast)
147147+ Unsubscribe(topic: String, client: Subject(Broadcast))
148148+}
149149+150150+fn pubsub_worker(
151151+ named: Name(PubSubMessage),
152152+) -> ChildSpecification(Subject(PubSubMessage)) {
153153+ let pubsub =
154154+ dict.new()
155155+ |> actor.new
156156+ |> actor.on_message(handle_pubsub_message)
157157+ |> actor.named(named)
158158+159159+ supervision.worker(fn() {
160160+ logging.log(logging.Info, "Starting pubsub worker")
161161+ actor.start(pubsub)
162162+ })
163163+}
164164+165165+fn handle_pubsub_message(state, message) {
166166+ case message {
167167+ Subscribe(topic:, client:) -> {
168168+ let new_state =
169169+ dict.upsert(in: state, update: topic, with: fn(clients) {
170170+ case clients {
171171+ Some(clients) -> [client, ..clients]
172172+ None -> {
173173+ logging.log(logging.Info, "Creating topic " <> topic)
174174+ [client]
175175+ }
176176+ }
177177+ })
178178+179179+ let assert Ok(pid) = process.subject_owner(client)
180180+ logging.log(
181181+ logging.Info,
182182+ "Subscribing client " <> pid_to_string(pid) <> " to topic " <> topic,
183183+ )
184184+185185+ actor.continue(new_state)
186186+ }
187187+ Publish(topic:, message:) -> {
188188+ case message {
189189+ Text(text) ->
190190+ logging.log(
191191+ logging.Info,
192192+ "Publishing text message `" <> text <> "` to topic " <> topic,
193193+ )
194194+ Bytes(binary) ->
195195+ logging.log(
196196+ logging.Info,
197197+ "Publishing binary message `"
198198+ <> string.inspect(binary)
199199+ <> "` to topic "
200200+ <> topic,
201201+ )
202202+ }
203203+204204+ case dict.get(state, topic) {
205205+ Ok(clients) -> list.each(clients, actor.send(_, message))
206206+ Error(_) -> Nil
207207+ }
208208+209209+ actor.continue(state)
210210+ }
211211+ Unsubscribe(topic:, client:) -> {
212212+ let assert Ok(pid) = process.subject_owner(client)
213213+ logging.log(
214214+ logging.Info,
215215+ "Unsubscribing client " <> pid_to_string(pid) <> " from topic " <> topic,
216216+ )
217217+218218+ let new_state = case dict.get(state, topic) {
219219+ Ok([_]) | Ok([]) -> {
220220+ logging.log(logging.Info, "Dropping topic " <> topic)
221221+ dict.drop(state, [topic])
222222+ }
223223+ Ok(clients) -> {
224224+ list.filter(clients, fn(c) { c != client })
225225+ |> dict.insert(state, topic, _)
226226+ }
227227+ Error(_) -> state
228228+ }
229229+230230+ actor.continue(new_state)
231231+ }
232232+ }
233233+}
234234+235235+// Utilities
236236+// -----------------------------------------------------------------------------
237237+238238+fn pid_to_string(pid: Pid) -> String {
239239+ charlist.to_string(pid_to_list(pid))
240240+}
241241+242242+@external(erlang, "erlang", "pid_to_list")
243243+fn pid_to_list(pid: Pid) -> Charlist
examples/sse/.gitignore
examples/.gitignore
-12
examples/sse/README.md
···11-# Example: Real-Time Gleam Chat
22-33-> [!NOTE]
44-> This example is taken from article [Building a real-time chat in Gleam](https://gautier.dev/articles/real-time-gleam-chat).
55-66-1. Start the server with the following command:
77-88-```bash
99-gleam run
1010-```
1111-1212-2. Open `http://127.0.0.1:8080/` in the browser
-13
examples/sse/gleam.toml
···11-name = "app"
22-version = "1.0.0"
33-44-[dependencies]
55-gleam_stdlib = ">= 0.44.0 and < 2.0.0"
66-gleam_erlang = ">= 1.3.0 and < 2.0.0"
77-gleam_http = ">= 4.2.0 and < 5.0.0"
88-gleam_otp = ">= 1.1.0 and < 2.0.0"
99-logging = ">= 1.3.0 and < 2.0.0"
1010-ewe = { path = "../../" }
1111-1212-[dev-dependencies]
1313-gleeunit = ">= 1.0.0 and < 2.0.0"