HTTP Endpoints
An http.endpoint maps an HTTP method and path to a Lua handler function.
Classification: configuration and API reference. YAML blocks are registry fragments that assume the referenced server, router, middleware, function entries, and security policies already exist. Lua blocks focus on handler contracts and identify application calls explicitly.
Definition
- name: get_user
kind: http.endpoint
meta:
router: app:api_router
method: GET
path: /users/{id}
func: app.users:get_user
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
meta.router |
registry.ID | Yes | Parent router (referenced by registry ID). |
method |
string | Yes | HTTP method, or "*" for any method |
path |
string | Yes | URL path pattern |
func |
registry.ID | Yes | Function to execute |
HTTP Methods
Supported methods:
| Method | Use Case |
|---|---|
GET |
Retrieve resources |
POST |
Create resources |
PUT |
Replace resources |
PATCH |
Partial update |
DELETE |
Remove resources |
HEAD |
Headers only |
OPTIONS |
CORS preflight (auto-handled) |
TRACE |
Diagnostic loopback |
* |
Any method |
Method names are uppercase; method is required, and any value outside this set is rejected as a configuration error.
Method-agnostic Endpoints
method: "*" registers the path for every HTTP method, and the handler reads the actual method with req:method():
- name: proxy
kind: http.endpoint
method: "*"
path: /proxy/{path...}
func: proxy_handler
For a normal endpoint the router also registers an OPTIONS handler on the same path, so CORS middleware can answer a preflight without the endpoint running. A * endpoint gets no such handler: it already matches OPTIONS. Router middleware still wraps it, so configured CORS middleware answers an allowed preflight with 204 before the endpoint runs; any other OPTIONS request reaches the endpoint function itself, which must answer it.
Path Parameters
Use {param} syntax for URL parameters:
- name: get_user
kind: http.endpoint
meta:
router: api
method: GET
path: /users/{id}
func: get_user
- name: get_user_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, req_err = http.request()
if req_err then return nil, req_err end
local user_id, user_err = req:param("user_id")
if user_err then return nil, user_err end
local post_id, post_err = req:param("post_id")
if post_err then return nil, post_err end
return {user_id = user_id, post_id = post_id}
end
Wildcard Paths
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 read like any other parameter, under the name without the trailing dots:
local req = http.request()
local tail = req:param("path") -- "docs/readme.md"
Handler Function
Endpoint functions obtain request and response objects from the http module:
local http = require("http")
local funcs = require("funcs")
local function handler()
local req, req_err = http.request()
if req_err then return nil, req_err end
local res, res_err = http.response()
if res_err then return nil, res_err end
local user_id, param_err = req:param("id")
if param_err then return nil, param_err end
local user, call_err = funcs.call("app.users:get_user", user_id)
if call_err then return nil, call_err end
local type_err = res:set_content_type(http.CONTENT.JSON)
if type_err then return nil, type_err end
local status_err = res:set_status(http.STATUS.OK)
if status_err then return nil, status_err end
local write_err = res:write_json(user)
if write_err then return nil, write_err end
return true
end
return { handler = handler }
Request Object
| Method | Returns | Description |
|---|---|---|
req:method() |
string | HTTP method |
req:path() |
string | Request path |
req:param(name) |
string | URL parameter |
req:params() |
table | All path parameters |
req:query(name) |
string | Query parameter |
req:query_params() |
table | All query parameters |
req:header(name) |
string | Request header |
req:headers() |
table | All request headers |
req:body() |
string | Request body |
req:body_json() |
table, error | Parse JSON body |
req:has_body() |
boolean | Check if body exists |
req:content_type() |
string | Content type |
req:content_length() |
number | Body size in bytes |
req:host() |
string | Hostname |
req:remote_addr() |
string | Client IP address |
req:accepts(type) |
boolean | Content negotiation |
req:is_content_type(type) |
boolean | Check content type |
req:stream() |
Stream | Body as stream for large files |
req:parse_multipart(max?) |
table, error | Parse multipart form |
Response Object
| Method | Description |
|---|---|
res:set_status(code) |
Set HTTP status code; returns an error if headers were sent |
res:set_header(name, value) |
Set response header; returns an error if headers were sent |
res:set_content_type(type) |
Set content type; returns an error if headers were sent |
res:write(data) |
Write raw body; returns an error on failure |
res:write_json(data) |
Write a JSON response; returns an error on failure |
res:write_event(data) |
Send and flush an SSE event; returns an error on failure |
res:set_transfer(encoding) |
Set chunked or sse transfer mode; returns an error if headers were sent |
res:flush() |
Flush the response; returns an error value |
JSON API Pattern
A JSON API handler can parse the request body, reject invalid input, and write a JSON result:
local http = require("http")
local funcs = require("funcs")
local function handler()
local req, req_err = http.request()
if req_err then return nil, req_err end
local res, res_err = http.response()
if res_err then return nil, res_err end
local data, err = req:body_json()
if err then
local status_err = res:set_status(http.STATUS.BAD_REQUEST)
if status_err then return nil, status_err end
local write_err = res:write_json({error = "Invalid JSON"})
if write_err then return nil, write_err end
return true
end
local result, process_err = funcs.call("app.api:process_request", data)
if process_err then return nil, process_err end
local status_err = res:set_status(http.STATUS.OK)
if status_err then return nil, status_err end
local write_err = res:write_json(result)
if write_err then return nil, write_err end
return true
end
return { handler = handler }
Error Responses
local http = require("http")
local funcs = require("funcs")
local function api_error(res, status, code, message)
local status_err = res:set_status(status)
if status_err then return nil, status_err end
local write_err = res:write_json({
error = {
code = code,
message = message
}
})
if write_err then return nil, write_err end
return true
end
local function handler()
local req, req_err = http.request()
if req_err then return nil, req_err end
local res, res_err = http.response()
if res_err then return nil, res_err end
local user_id, param_err = req:param("id")
if param_err then return nil, param_err end
local user, err = funcs.call("app.users: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
local status_err = res:set_status(http.STATUS.OK)
if status_err then return nil, status_err end
local write_err = res:write_json(user)
if write_err then return nil, write_err end
return true
end
return { handler = handler }
Examples
CRUD Endpoints
entries:
- name: users_router
kind: http.router
meta:
server: gateway
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
Protected Endpoint
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
See Also
- Router - Route grouping
- HTTP Module - Request/response API
- Middleware - Request processing