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. The datetime library gives you nanosecond-precision timestamps. String interpolation formats it as RFC 3339 ("2026-05-27T15:30:05Z") because Instant conforms to Formattable.
  • SharedDatabase("notes.db") opens a SQLite connection that returns a Result — we match on it to handle the case where the file can't be opened.
  • App[AppCtx] is generic over the context type. Every handler receives a Request and the context, and returns a Response.
  • Logger[AppCtx]() is middleware that prints each request to the console. The use method adds it to the pipeline.
  • app.route(get: "/", hello) registers hello as the handler for GET /. The labeled parameter get: tells Perch the HTTP method.
  • app.listen returns a Result. We match on 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

StepFeature
1Flock project setup, flock.toml dependencies
2Structs, Cloneable protocol conformance, SharedDatabase
3Generics (App[AppCtx]), match on Result, Instant.now(), Formattable interpolation
4flock run, verifying with curl