Skip to content

HTTP Endpoints

Endpoints (http.endpoint) define HTTP route handlers that execute Lua functions.

- name: get_user
kind: http.endpoint
meta:
router: app:api_router
method: GET
path: /users/{id}
func: app.users:get_user
FieldTypeRequiredDescription
meta.routerregistry.IDYesParent router (referenced by registry ID).
methodstringYesHTTP method
pathstringYesURL path pattern
funcregistry.IDYesFunction to execute

Supported methods:

MethodUse Case
GETRetrieve resources
POSTCreate resources
PUTReplace resources
PATCHPartial update
DELETERemove resources
HEADHeaders only
OPTIONSCORS preflight (auto-handled)
TRACEDiagnostic loopback

Use {param} syntax for URL parameters:

- name: get_user
kind: http.endpoint
method: GET
path: /users/{id}
func: get_user
- name: get_user_post
kind: http.endpoint
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("id")
local post_id = req:param("post_id")
end

Use {path...} to match any remaining path segments:

- name: file_handler
kind: http.endpoint
method: GET
path: /files/{path...}
func: serve_file

This catch-all segment makes the route match requests like /files/docs/readme.md. The captured tail is not currently exposed to Lua: req:param("path") returns nil for the wildcard value. Read req:path() if you need the full request path.

Endpoint functions obtain request and response objects from the http module:

local http = require("http")
local json = require("json")
local function handler()
local req = http.request()
local res = http.response()
-- Read request
local body = req:body()
local user_id = req:param("id")
local page = req:query("page")
local auth = req:header("Authorization")
-- Process
local user = get_user(user_id)
-- Write response
res:set_content_type(http.CONTENT.JSON)
res:set_status(http.STATUS.OK)
res:write_json(user)
end
return { handler = handler }
MethodReturnsDescription
req:method()stringHTTP method
req:path()stringRequest path
req:param(name)stringURL parameter
req:params()tableAll path parameters
req:query(name)stringQuery parameter
req:query_params()tableAll query parameters
req:header(name)stringRequest header
req:body()stringRequest body
req:body_json()table, errorParse JSON body
req:has_body()booleanCheck if body exists
req:content_type()stringContent type
req:content_length()numberBody size in bytes
req:host()stringHostname
req:remote_addr()stringClient IP address
req:accepts(type)booleanContent negotiation
req:is_content_type(type)booleanCheck content type
req:stream()StreamBody as stream for large files
req:parse_multipart(max?)table, errorParse multipart form
MethodDescription
res:set_status(code)Set HTTP status code
res:set_header(name, value)Set response header
res:set_content_type(type)Set content type
res:write(data)Write raw body
res:write_json(data)Write JSON response
res:write_event(data)Send SSE event
res:set_transfer(encoding)Set transfer mode (SSE, chunked)
res:flush()Flush response to client

Common pattern for JSON APIs:

local http = require("http")
local function handler()
local req = http.request()
local res = http.response()
local data, err = req:body_json()
if err then
res:set_status(http.STATUS.BAD_REQUEST)
res:write_json({error = "Invalid JSON"})
return
end
local result = process(data)
res:set_status(http.STATUS.OK)
res:write_json(result)
end
return { handler = handler }
local http = require("http")
local function api_error(res, status, code, message)
res:set_status(status)
res:write_json({
error = {
code = code,
message = message
}
})
end
local function handler()
local req = http.request()
local res = http.response()
local user_id = req:param("id")
local user, err = db.get_user(user_id)
if err then
if errors.is(err, errors.NOT_FOUND) then
return api_error(res, http.STATUS.NOT_FOUND, "USER_NOT_FOUND", "User not found")
end
return api_error(res, http.STATUS.INTERNAL_ERROR, "INTERNAL_ERROR", "Server error")
end
res:set_status(http.STATUS.OK)
res:write_json(user)
end
return { handler = handler }
entries:
- name: users_router
kind: http.router
prefix: /api/users
middleware:
- cors
- compress
- name: list_users
kind: http.endpoint
meta:
router: users_router
method: GET
path: /
func: app.users:list
- name: get_user
kind: http.endpoint
meta:
router: users_router
method: GET
path: /{id}
func: app.users:get
- name: create_user
kind: http.endpoint
meta:
router: users_router
method: POST
path: /
func: app.users:create
- name: update_user
kind: http.endpoint
meta:
router: users_router
method: PUT
path: /{id}
func: app.users:update
- name: delete_user
kind: http.endpoint
meta:
router: users_router
method: DELETE
path: /{id}
func: app.users:delete

Authorization middleware is configured on the parent router, not on the endpoint. Post-match middleware (such as endpoint_firewall) runs after route matching and applies to every endpoint under the router:

- name: admin_router
kind: http.router
meta:
server: gateway
prefix: /admin
middleware:
- cors
- token_auth
post_middleware:
- endpoint_firewall
post_options:
endpoint_firewall.action: "admin"
- name: admin_endpoint
kind: http.endpoint
meta:
router: admin_router
method: POST
path: /settings
func: app.admin:update_settings