Project Setup
By the end of this page you'll have a Perch web server that responds to HTTP requests with JSON. It's about 20 lines of Kestrel.
Step 1 — Create the project
mkdir notes-app && cd notes-app
flock init
Open flock.toml and add the dependencies we'll use throughout the guide. Registry dependencies are keyed by org/name:
[package]
name = "notes-app"
version = "0.1.0"
[dependencies]
kestrel/perch = { version = "0.3.0" }
kestrel/talon-sqlite = { version = "0.1.0" }
kestrel/quill = { version = "0.2.1" }
kestrel/quill-json = { version = "0.2.2" }
kestrel/http = { version = "0.2.2" }
kestrel/html-builder = { version = "0.1.0" }
kestrel/datetime = { version = "0.1.0" }
kestrel/crypto = { version = "0.1.0" }
We won't use all of these yet — Perch is the web framework, and the others come into play when we add a database, HTML rendering, timestamps, and authentication in later pages.
Step 2 — App context
Every Perch app carries a context value that gets passed to every handler. This is where you put shared state — in our case, the database connection. Create src/context.ks:
module notes.context
import talon.sqlite.shared_database.(SharedDatabase)
public struct AppCtx: Cloneable {
public var db: SharedDatabase
public func clone() -> AppCtx {
AppCtx(db: self.db.clone())
}
}
AppCtx conforms to Cloneable because Perch copies the context for each request. SharedDatabase is a reference-counted SQLite connection — cloning bumps the refcount and shares the underlying handle, so every handler works with the same connection without opening a new one per request.
Step 3 — Hello endpoint
Replace the contents of src/main.ks with:
module notes.main
import perch.app.(App)
import perch.request.(Request)
import perch.response.(Response)
import perch.middleware.(Logger)
import perch.json_body.(JsonBody)
import quill.value.(Value)
import talon.sqlite.shared_database.(SharedDatabase)
import datetime.(Instant)
import notes.context.(AppCtx)
func hello(req: Request, ctx: AppCtx) -> Response {
let now = Instant.now();
var obj = Dictionary[String, Value]();
obj.insert("status", Value.Str("ok"));
obj.insert("time", Value.Str("\(now)"));
Response.ok(JsonBody(fromRaw: Value.Obj(obj)))
}
@main
func main() {
let db = match SharedDatabase("notes.db") {
.Ok(d) => d,
.Err(e) => {
println("Failed to open database: " + e.description());
return
}
};
let ctx = AppCtx(db: db);
var app = App[AppCtx](ctx);
app.use(Logger[AppCtx]());
app.route(get: "/", hello);
let port: UInt16 = 8080;
println("Notes app listening on http://localhost:8080");
match app.listen(port) {
.Ok(_) => {},
.Err(e) => { println("Error: " + e.description()); }
}
}
A few things to notice:
Instant.now()captures the current UTC time. Thedatetimelibrary gives you nanosecond-precision timestamps. String interpolation formats it as RFC 3339 ("2026-05-27T15:30:05Z") becauseInstantconforms toFormattable.SharedDatabase("notes.db")opens a SQLite connection that returns aResult— wematchon it to handle the case where the file can't be opened.App[AppCtx]is generic over the context type. Every handler receives aRequestand the context, and returns aResponse.Logger[AppCtx]()is middleware that prints each request to the console. Theusemethod adds it to the pipeline.app.route(get: "/", hello)registershelloas the handler forGET /. The labeled parameterget:tells Perch the HTTP method.app.listenreturns aResult. Wematchon it to handle startup failures — if the port is already in use, for example, the error branch prints the reason.
Step 4 — Run it
flock run
In another terminal:
curl http://localhost:8080/
You should see:
{"status":"ok","time":"2026-05-27T15:30:05Z"}
The server is running and returning the current UTC time. In the next page we'll add a database behind it.
What you saw
| Step | Feature |
|---|---|
| 1 | Flock project setup, flock.toml dependencies |
| 2 | Structs, Cloneable protocol conformance, SharedDatabase |
| 3 | Generics (App[AppCtx]), match on Result, Instant.now(), Formattable interpolation |
| 4 | flock run, verifying with curl |