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