Build a REST API
A step-by-step tutorial: build a small JSON REST API with routes, middleware, and security using serez-http, then ship it as a Docker image with serez-apipack.
GET/POST routes, route parameters, logging and auth middleware, rate limiting, then a deployable Docker container.Step 1 — Set up
mkdir tasks-api
cd tasks-api
sz init --y
sz install serez-httpsz init --y creates a serez.json with a dev script (sz index.sz), so we'll start the server with sz run dev. serez-http declares the Socket and Time permissions it needs in its own modules, so your app doesn't have to. Create index.sz and create the app:
import "serez-http"
const app = new App()Step 2 — Your first route
A route is an HTTP verb, a path, and a handler that receives req and res. Send JSON with res.json():
app.GET("/", fn(req, res) {
// dicts are typed: <string, any> with ({"key", value}) entries
let r <string, any> = ({"message", "tasks API"}, {"version", "1.0"})
res.json(r)
})
app.listen(3000, fn() {
out "listening on http://127.0.0.1:3000"
})Run it with the dev script and hit it from another terminal:
sz run dev
# elsewhere:
curl http://127.0.0.1:3000/get and use are reserved words in Serez Code, so the methods are GET/POST/PUT/delete and middleware is registered with addMw.Step 3 — Route parameters and a data store
Use :param segments and read them from req["params"]. We'll keep tasks in a simple in-memory array:
let tasks = ["buy milk", "write docs", "ship release"]
// List all tasks
app.GET("/tasks", fn(req, res) {
let r <string, any> = ({"tasks", tasks}, {"count", tasks.length()})
res.json(r)
})
// Get one task by index: /tasks/0
app.GET("/tasks/:id", fn(req, res) {
let id = parseInt(req["params"]["id"])
if (id < 0 || id >= tasks.length()) {
let e <string, any> = ({"error", "no such task"})
res.status(404).json(e)
} else {
let r <string, any> = ({"id", id}, {"task", tasks[id]})
res.json(r)
}
})Step 4 — Handle POST with a JSON body
The raw body is in req["body"]. Parse it with JSON.parse:
app.POST("/tasks", fn(req, res) {
let body = JSON.parse(req["body"])
let title = body["title"]
if (title == null) {
let e <string, any> = ({"error", "title is required"})
res.status(400).json(e)
} else {
tasks.push(title)
let r <string, any> = ({"added", title}, {"count", tasks.length()})
res.status(201).json(r)
}
})curl -X POST http://127.0.0.1:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "review PR"}'Step 5 — Middleware: logging and auth
Middleware runs before handlers. Register it with addMw; call next() to continue, or send a response to stop the chain. Order matters — register middleware before listen:
// Log every request
app.addMw(fn(req, res, next) {
out req["method"] + " " + req["path"]
next()
})
// Require a token on write routes
app.addMw(fn(req, res, next) {
if (req["method"] == "POST" && req["authorization"] == "") {
let e <string, any> = ({"error", "unauthorized"})
res.status(401).json(e)
} else {
next()
}
})Step 6 — Security: rate limiting and CORS
Throttle abusive clients with the built-in per-route rate limiter, and control which origins may call your API with app.cors. Both are native App methods — the old corsMiddleware / _rateLimit helpers are deprecated:
// Max 100 requests per minute on /tasks — exceeding it answers 429 automatically
app.rateLimitRoute("/tasks", 100, 60)
// CORS headers on every response + automatic preflight (OPTIONS → 204)
app.cors("https://my-frontend.com", "GET, POST, PUT, DELETE", "Content-Type, Authorization")
// You can also set a global header on all responses
app.header("X-Powered-By", "serez-http")Socket does not expose the peer address yet).Step 7 — Handle errors
Register a global error handler so a thrown exception in a handler returns a clean 500 instead of crashing the server:
app.error(fn(err, req, res) {
out "error: " + err
let e <string, any> = ({"error", "internal server error"})
res.status(500).json(e)
})Step 8 — Deploy with serez-apipack
serez-apipack packages the project into a self-contained Docker image — the host only needs Docker, not Serez Code. Your serez.json already has a dev script from sz init; declare the dependency and the full dev/build script pair:
{
"name": "tasks-api",
"version": "1.0.0",
"main": "index.sz",
"scripts": {
"dev": "sz index.sz",
"build": "sz run apipack tag=tasks-api:1.0.0"
},
"dependencies": {
"serez-http": "1.0.4",
"serez-apipack": "1.1.4"
}
}Now the workflow is two commands: sz run dev to develop locally, and sz run build to produce the Docker image. Install the packer and build:
sz install # installs serez-http + serez-apipack from serez.json
sz run dev # local development server
sz run build # → runs the apipack packer, builds tasks-api:1.0.0
# you can also invoke the packer directly (options are key=value, no dashes):
sz run apipack port=8080 tag=tasks-api:latestRun the container — it's reachable from outside the host:
docker run -p 3000:3000 tasks-api:1.0.0127.0.0.1, so apipack adds a socat proxy inside the image that forwards 0.0.0.0:PORT → the app. You write a local server; apipack makes it reachable.Next steps
- Add WebSocket endpoints with
app.ws(path, fn(req, ws){})(core v4.5.0+). - See the serez-http reference and serez-apipack reference.
- Serve a model: combine this with the GPT agent tutorial to expose inference over HTTP.