|
Routing Parameter extraction, method handlers. |
Middleware CORS, body parsing, rate limiting, JWT support. |
|
Realtime Server-Sent Events with pub/sub broker. |
Async I/O Coroutine-based with keep-alive. No external event loop. |
luarocks install moteTip
Works with Lua 5.1–5.4. LuaJIT recommended for performance.
local mote = require("mote")
mote.get("/", function(ctx)
ctx.response.body = { message = "Hello, World!" }
end)
mote.get("/users/:id", function(ctx)
ctx.response.body = { id = ctx.params.id }
end)
mote.post("/echo", function(ctx)
ctx.response.body = ctx.request.body
end)
local app = mote.create({ port = 8080 })
print("Listening on http://localhost:8080")
app:run()mote.get(path, handler)
mote.post(path, handler)
mote.put(path, handler)
mote.patch(path, handler)
mote.delete(path, handler)
mote.all(path, handler)Routes support parameters:
mote.get("/users/:id/posts/:post_id", function(ctx)
print(ctx.params.id, ctx.params.post_id)
end)Set response via ctx.response:
ctx.response.body = { id = 1 } -- auto JSON, auto 200
ctx.response.body = "hello" -- text/plain
ctx.response.status = 201 -- override status
ctx.response.type = "text/html" -- override content-type
ctx:set("X-Custom", "val") -- set response header
ctx:append("Link", "<...>") -- append to header
ctx:remove("X-Custom") -- remove response header
ctx:redirect("/login") -- 302 redirect
ctx:cookie("session", "abc123", { httpOnly = true })
ctx:throw(401, "unauthorized") -- set status/body and stop
ctx:assert(user, 401, "login required") -- assert or throwStatic Files
local mime_types = {
css = "text/css",
js = "application/javascript",
png = "image/png",
svg = "image/svg+xml",
}
mote.get("/static/:file", function(ctx)
local filename = ctx.params.file
if filename:match("%.%.") then ctx:throw(400, "invalid path") end
local f = io.open("./public/" .. filename, "rb")
if not f then ctx:throw(404, "not found") end
local data = f:read("*a")
f:close()
local ext = filename:match("%.(%w+)$")
ctx.response.type = mime_types[ext] or "application/octet-stream"
ctx:set("Content-Disposition", "inline; filename=" .. filename)
ctx.response.body = data
end)[!TIP] For production, serve static files via Nginx or a CDN for better performance and caching.
Cookies
Set cookies with ctx:cookie():
ctx:cookie("session", "abc123", {
path = "/",
httpOnly = true,
secure = true,
sameSite = "Strict",
maxAge = 86400,
})Read cookies from ctx.cookies:
local session = ctx.cookies.sessionContext Object
The ctx object passed to handlers:
-- Request
ctx.request.method -- HTTP method
ctx.request.path -- URL path
ctx.request.headers -- Request headers (lowercase keys)
ctx.request.body -- Parsed body (JSON or multipart)
ctx.params -- Route parameters
ctx.query -- Parsed query params (lazy)
ctx.cookies -- Parsed cookies (lazy)
ctx.url -- Full URL (path + query string)
ctx.ip -- Client IP address
ctx.user -- JWT payload (if authenticated)
ctx:get("Header") -- Get request header (case-insensitive)
-- Response
ctx.response.body -- Response body (table=JSON, string=text)
ctx.response.status -- HTTP status (auto 200 with body, 204 without)
ctx.response.type -- Content-Type override
-- Shared state
ctx.state -- Pass data between middleware
ctx.config -- Server configMiddleware
Onion-style middleware with next():
mote.use(function(ctx, next)
local start = os.clock()
next() -- call downstream
local ms = (os.clock() - start) * 1000
ctx:set("X-Response-Time", string.format("%.3fms", ms))
end)
mote.use(function(ctx, next)
if not ctx.user then
ctx:throw(401, "unauthorized")
end
next()
end)Server-Sent Events
local broker = mote.pubsub.broker
mote.get("/events", function(ctx)
local client = broker.create_client()
client:subscribe("messages")
mote.sse(ctx, client)
end)
broker.broadcast("messages", "new", { text = "Hello!" })Server Options
local app = mote.create({
host = "0.0.0.0",
port = 8080,
secret = "your-jwt-secret", -- or set MOTE_SECRET env var
timeout = 30,
keep_alive_timeout = 5,
keep_alive_max = 1000,
max_concurrent = 10000,
ratelimit = true,
reuseaddr = true, -- allow binding to address in TIME_WAIT
reuseport = true, -- allow multiple processes to bind same port
})
app:on_tick(function()
-- runs every event loop iteration
end)
app:run()
app:stop() -- immediate shutdown
app:stop(5) -- graceful: drain connections for 5 secondsCORS & Rate Limiting
mote.configure_cors({
origin = "https://example.com",
methods = "GET, POST",
headers = "Content-Type, Authorization",
})
mote.ratelimit_configure({
["/api/login"] = { max = 10, window = 60 },
["*"] = { max = 100, window = 60 },
})Advanced
-- Manual event loop control (for embedding)
while true do
app:step(0.1) -- process events, 100ms timeout
end
-- Monitoring
local count = app.active_connections()
-- Direct client messaging
broker.send_to_client(client_id, "notification", { text = "Hello!" })
-- SSE permission checker (called on each broadcast)
broker.set_permission_checker(function(client, topic, record)
return client:get_auth() ~= nil -- only authenticated clients
end)
-- Dynamic subscriptions
client:subscribe("posts")
client:unsubscribe("posts")
-- JWT validation hooks
mote.set_user_validator(function(sub)
local user = db.get_user(sub)
if not user then return nil, "user not found" end
return true
end)
mote.set_issuer_resolver(function()
return os.getenv("JWT_ISSUER") -- dynamic issuer for validation
end)
-- Global rate limit override (0 disables rate limiting)
mote.ratelimit_set_global(1000)
-- Logging control
local log = require("mote.log")
log.set_level("debug") -- debug, info, warn, error
log.disable() -- or set MOTE_LOG=0 env var
-- Submodules
local parser = require("mote.parser")
local jwt = require("mote.jwt")
local crypto = require("mote.crypto")
local url = require("mote.url")Mote serves HTTP only. For production, use a reverse proxy for TLS termination:
Internet → Caddy/Nginx (HTTPS) → Mote (HTTP)
Cloud platforms like Fly.io, Railway, and Render handle TLS at the edge automatically.
luarocks make # Build
busted # Tests
luacheck . # Lint
stylua . # Format- ena-api — Compile API for the Ena programming language
- lpeg_patterns by daurnimator (MIT)
- hmac_sha256 by h5p9sl (Unlicense)
- luasocket-poll-api-test by FreeMasen (MIT)
Note
This library was written with assistance from LLMs. Human review and guidance provided where needed.