Roundabout

Roundabout

A route generator for Gleam.

Package Version Hex Docs

Why

Gleam gives us a great way of matching paths via pattern matching:

pub type Route {
  Users
  User(id: String)
  NotFound
}

pub fn get_route(segments: List(String)) {
  case segments {
    ["users"] -> Users
    ["users", id] -> User(id)
    _ -> NotFound
  }
}

However, this doesn’t provide a type safe way of constructing a path from type e.g.

User("12") -> "/users/12"

See https://www.kurz.net/posts/gleam-routing for a more detailed explanation.

This packages provides a generator which gives you:

This generator can be used in frontend or backend applications, at it only generates the route types and helper functions. You still need to write your own router using these types.

Install

gleam add roundabout@1

Generating routes

Create a module in your project which defines the route definitions and calls the generator. e.g. in src/gen_routes.gleam

import roundabout.{Int, Lit, Route, Str}

const routes = [
  Route(name: "home", path: [], sub: []),
  // Will match an individual order e.g. /orders/123
  Route(name: "order", path: [Lit("orders"), Int("id")], sub: []),
  Route(
    name: "user",
    path: [Lit("users"), Int("id")],
    sub: [
      // Will match /users/123
      Route(name: "show", path: [], sub: []),
      // Will match /users/123/delete
      Route(name: "delete", path: [Lit("delete")], sub: []),
    ],
  ),
]

pub fn main() -> Nil {
  roundabout.main(routes, "src/generated/routes")
}

Call this using:

gleam run -m gen_routes

See example output at examples/src/generated/routes.gleam

Using this in your application

After the routes have been generated, you can use them in your router or views like:

import generated/routes

pub fn handle(segments: List(String)) {
  let maybe_route = routes.segments_to_route(segments)

  case maybe_route {
    Ok(routes.Home) -> handle_home()
    Ok(routes.Order(id)) -> handle_order(id)
    ...
    Error(_) -> handle_not_found()
  }
} 

Notes

The order is important

If you have routes like:

[
  Route(name: "show", path: [Str("id")], sub: []),
  Route(name: "invite", path: [Lit("invite")], sub: []),
]

The first one will always match over the second one, make sure that literal routes are first.

Structure your routes to support your middleware

If you want to use different middlewares at different levels of your application, you can structure your routes to support this.

For example, having:

const routes = [
  Route(name: "home", path: [], sub: []),
  Route(
    name: "app",
    path: [Lit("app")],
    sub: [
      // Will match /app/
      Route(name: "dashboard", path: [], sub: []),
    ],
  ),
]

Allows to apply some middleware for authentication like:

import generated/routes
import middleware
import wisp

pub fn handle(req: Request,, ctx: Context) {
  let segments = wisp.path_segments(req)
  let maybe_route = routes.segments_to_route(segments)

  case maybe_route {
    Ok(routes.Home) -> handle_home()
    Ok(routes.App(sub)) -> {
      use authenticated_context <- middleware.require_session(req, ctx)
    
      case sub {
        routes.Dashboard -> handle_dashboard(authenticated_context)
      }
    }
    ...
    Error(_) -> handle_not_found()
  }
} 

Further documentation can be found at https://hexdocs.pm/roundabout.

Search Document