Web framework

lxweb

Web apps that stay up

Phoenix-inspired web framework for Lx. Declare routes with scopes and pipelines, respond from controllers, render HTML views and ship real-time interfaces with LiveView over WebSockets — all running on Cowboy, on the BEAM, with gettext i18n and dev hot-reload built in.

v0.1.0 ~85% complete

Highlights

  • Router with scopes & pipelines
  • Controllers: JSON, text, render, redirect
  • LiveView over WebSockets
  • Path params (:id)
  • gettext i18n
  • Dev hot-reload + E2E test client

Features

Routing

Group routes into scopes under a URL prefix, each through its own pipeline of plugs. Path params like /posts/:id are extracted automatically.

Pipelines

Composable plugs per scope: fetch_params, fetch_session, put_secure_headers, accept_json, logger — chain what you need.

Controllers

Plain functions (conn, params) -> conn. Respond with json, text, render, redirect, send_resp, set status and headers, or halt the pipeline.

Views & templates

render/2 modules with the template directive (layouts, partials) and component tags. Keep markup out of your logic.

LiveView

Reactive components: the server holds state and pushes HTML patches over WebSockets. Bindings lx-click / lx-change / lx-submit; morphdom applies diffs on the client.

Testing

lxweb_request simulates full HTTP requests through your real router — no TCP. Assert status, body and headers in plain tests.

Examples

Router: scopes & pipelines

require "lxweb_router"
require "lxweb_pipeline"

def dispatch(conn, method, path) do
  pipelines = %{
    browser: [
      {:lxweb_pipeline, :fetch_params, []},
      {:lxweb_pipeline, :put_secure_headers, []}
    ],
    api: [{:lxweb_pipeline, :accept_json, []}]
  }
  routes = [
    {:scope, "/api", :api, [
      {:get,  "/posts",      :post_controller, :list},
      {:post, "/posts",      :post_controller, :create}
    ]},
    {:scope, "/", :browser, [
      {:get,  "/",           :page_controller, :index},
      {:get,  "/posts/:id",  :post_controller, :show},
      {:live, "/counter",    :counter_live}
    ]}
  ]
  lxweb_router:dispatch(conn, method, path, pipelines, routes)
end

Controller: JSON, params, redirect

require "lxweb_controller"

def show(conn, params) do
  id = params[:id]
  lxweb_controller:json(conn, %{id: id, title: "Hello"})
end

def create(conn, params) do
  conn
    |> lxweb_controller:put_status(201)
    |> lxweb_controller:json(%{ok: true, title: params[:title]})
end

LiveView: counter over WebSockets

require "lxweb/lxweb_live"

def mount(_params, _session, socket) do
  {:ok, lxweb_live:assign(socket, %{count: 0})}
end

def handle_event("inc", _params, socket) do
  c = socket.assigns[:count]
  {:noreply, lxweb_live:assign(socket, %{count: c + 1})}
end

def render(assigns) do
  ~L"""
  <div data-lx-topic="lv:counter">
    <h2>Count: #{assigns.count}</h2>
    <button lx-click="inc">+1</button>
  </div>
  """
end

E2E test (no TCP)

require "lxweb_request"
require "lxweb_conn"

test "GET /posts/:id" do
  conn = lxweb_conn:test(:my_router)
  r = lxweb_request:get(conn, "/posts/42")
  assert r.status == 200
  assert lxweb_request:response_header(r, "content-type") == "application/json"
end

Install

# project.yml
dependencies:
  lxweb:
    path: ../../lx_libs/lxweb
  cowboy: "~> 2.0"

# then
$ lx deps get & lx compile & lx run