Skip to content

Routing

Routers group endpoints under URL prefixes and apply shared middleware. Endpoints define HTTP handlers.

flowchart TB
S[http.service<br/>:8080] --> R1[http.router<br/>/api]
S --> R2[http.router<br/>/admin]
S --> ST[http.static<br/>/]
R1 --> E1[GET /users]
R1 --> E2[POST /users]
R1 --> E3["GET /users/{id}"]
R2 --> E4[GET /stats]
R2 --> E5[POST /config]

Entries reference parents via metadata:

  • Routers: meta.server: app:gateway
  • Endpoints: meta.router: app:api
- name: api
kind: http.router
meta:
server: gateway
prefix: /api/v1
middleware:
- cors
- compress
options:
cors.allow.origins: "*"
post_middleware:
- endpoint_firewall
FieldTypeDescription
meta.serverRegistry IDParent HTTP server
prefixstringURL prefix for all routes
middleware[]stringPre-match middleware
optionsmapMiddleware options
post_middleware[]stringPost-match middleware
post_optionsmapPost-match middleware options
- name: get_user
kind: http.endpoint
meta:
router: api
method: GET
path: /users/{id}
func: app.users:get_user
FieldTypeDescription
meta.routerRegistry IDParent router
methodstringHTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
pathstringURL path pattern (starts with /)
funcRegistry IDHandler function

Use {param} syntax for URL parameters:

- name: get_post
kind: http.endpoint
meta:
router: api
method: GET
path: /users/{user_id}/posts/{post_id}
func: get_user_post

Access in handler:

local http = require("http")
local function handler()
local req = http.request()
local user_id = req:param("user_id")
local post_id = req:param("post_id")
-- ...
end

Capture remaining path segments with {param...}:

- name: serve_files
kind: http.endpoint
meta:
router: api
method: GET
path: /files/{filepath...}
func: serve_file

The wildcard matches the remaining segments for routing purposes, so a request like GET /api/v1/files/docs/guides/readme.md is matched and dispatched to the handler. The captured tail itself is not currently retrievable via req:param.

The wildcard must be the last segment in the path.

Endpoint handlers use the http module to access request and response objects. See HTTP Module for the complete API.

local http = require("http")
local function handler()
local req = http.request()
local res = http.response()
local user_id = req:param("id")
local user = get_user(user_id)
res:set_status(http.STATUS.OK)
res:write_json(user)
end
return { handler = handler }

Middleware options use dot notation with the middleware name as prefix:

middleware:
- cors
- ratelimit
- token_auth
options:
cors.allow.origins: "https://app.example.com"
cors.allow.methods: "GET,POST,PUT,DELETE"
ratelimit.requests: "100"
ratelimit.window: "1m"
token_auth.store: "app:tokens"
token_auth.header.name: "Authorization"

Post-match middleware uses post_options:

post_middleware:
- endpoint_firewall
post_options:
endpoint_firewall.action: "access"

Pre-match (middleware) runs before route matching:

  • CORS (handles OPTIONS preflight)
  • Compression
  • Rate limiting
  • Real IP detection
  • Token authentication (context enrichment)

Post-match (post_middleware) runs after route is matched:

  • Endpoint firewall (needs route info for authorization)
  • Resource firewall
  • WebSocket relay
middleware: # Pre-match: all requests to this router
- cors
- compress
- token_auth # Enriches context with actor/scope
post_middleware: # Post-match: matched routes only
- endpoint_firewall # Uses actor from token_auth
Token authentication can be pre-match because it only enriches context—it doesn't block requests. Authorization happens in post-match middleware like endpoint_firewall which uses the actor set by token_auth.
version: "1.0"
namespace: app
entries:
# Server
- name: gateway
kind: http.service
addr: ":8080"
lifecycle:
auto_start: true
# API Router
- name: api
kind: http.router
meta:
server: gateway
prefix: /api/v1
middleware:
- cors
- compress
- ratelimit
options:
cors.allow.origins: "https://app.example.com"
ratelimit.requests: "100"
ratelimit.window: "1m"
# Handler function
- name: get_users
kind: function.lua
source: file://handlers/users.lua
method: list
modules:
- http
- json
- sql
# Endpoints
- name: list_users
kind: http.endpoint
meta:
router: api
method: GET
path: /users
func: get_users
- name: get_user
kind: http.endpoint
meta:
router: api
method: GET
path: /users/{id}
func: app:get_user_by_id
- name: create_user
kind: http.endpoint
meta:
router: api
method: POST
path: /users
func: app:create_user

Common pattern with authentication:

entries:
# Public routes (no auth)
- name: public
kind: http.router
meta:
server: gateway
prefix: /api/public
middleware:
- cors
# Protected routes
- name: protected
kind: http.router
meta:
server: gateway
prefix: /api
middleware:
- cors
- token_auth
options:
token_auth.store: app:tokens
post_middleware:
- endpoint_firewall