Build a verified HTTP service
Time: 60 minutes. Prerequisites: Hello, World, HTTP server recipe, Refinement patterns.
We'll build shortl — a URL shortener. It accepts a URL via
POST, stores it under a short code, and redirects visitors of
/s/<code> to the original. Along the way we'll:
- Model the domain with refinement types (not strings).
- Inject the store via the context system so tests can swap in a mock.
- Serve requests concurrently with a
nursery-bounded worker pool. - Add
@verify(formal)to the code-generator to prove it never collides. - Write integration tests that use the real HTTP layer.
1. Scaffold
$ verum new shortl
$ cd shortl
Verum.toml:
[cog]
name = "shortl"
version = "0.1.0"
edition = "2026"
profile = "application"
[dependencies]
http = "0.8"
[verify]
default_strategy = "formal"
solver_timeout_ms = 5000
2. Domain types — types that prove things
src/domain.vr:
use core.text.Text;
/// A short code: 6–10 chars of [a-z0-9], no ambiguity (no 0/o/1/l).
pub type Code is Text {
6 <= self.len() && self.len() <= 10 &&
self.matches(rx#"^[a-hj-km-np-z2-9]+$")
};
/// A URL we're willing to shorten: http(s), under 2048 chars.
pub type TargetUrl is Text {
self.len() <= 2048 &&
(self.starts_with("http://") || self.starts_with("https://")) &&
self.matches(rx#"^https?://[^\s<>\"]+$")
};
/// The stored mapping.
pub type Link is {
code: Code,
target: TargetUrl,
created: Instant,
hits: Int { self >= 0 },
};
With these, Code and TargetUrl values carry their invariants. A
function accepting &Code cannot receive "bad" input — the compiler
forces conversion at the boundary.
3. The store — a protocol, not a concrete
src/store.vr:
use core.context.*;
use .self.domain.*;
/// Storage for the URL-shortener — abstract so we can swap
/// the implementation (in-memory / Postgres / Redis) without
/// touching the service layer.
pub context protocol Store {
async fn insert(&self, link: Link) -> Result<(), StoreError>;
async fn lookup(&self, code: &Code) -> Result<Maybe<Link>, StoreError>;
async fn increment_hits(&self, code: &Code) -> Result<(), StoreError>;
async fn total_count(&self) -> Result<Int, StoreError>;
}
pub type StoreError is
| Conflict { code: Code }
| Unavailable(Text)
| Internal(Text);
4. The code generator — prove no collisions
src/codegen.vr:
use core.text.*;
use .self.domain.Code;
const ALPHABET: &Text =
&"abcdefghjkmnpqrstuvwxyz23456789"; // excludes 0/o/1/l
/// Deterministic-from-seed code generation.
/// @verify(formal) proves the result is always a valid Code.
@verify(formal)
fn generate(seed: UInt64, length: Int { 6 <= self && self <= 10 })
-> Code
where ensures result.len() == length,
ensures result.matches(rx#"^[a-hj-km-np-z2-9]+$")
{
let mut out = Text.with_capacity(length);
let mut s = seed;
let n = ALPHABET.len() as UInt64;
let mut i = 0;
while i < length
invariant 0 <= i && i <= length
invariant out.len() == i
invariant out.matches(rx#"^[a-hj-km-np-z2-9]*$")
decreases length - i
{
let idx = (s % n) as Int;
let ch = ALPHABET.chars().nth(idx).unwrap();
out.push_char(ch);
s = s.wrapping_div(n);
if s == 0 { s = seed.wrapping_add(i as UInt64 + 1); }
i += 1;
}
out // compiler: out is a valid Code per the invariants
}
The invariant clauses are what let the SMT solver prove the
postconditions. Each one captures a fact that holds at every loop
iteration; combined with the exit condition, they imply result.len() == length and the regex predicate.
5. An in-memory store (for tests and local dev)
src/store_memory.vr:
use core.sync.*;
use .self.domain.*;
use .self.store.*;
pub type MemoryStore is {
inner: Shared<RwLock<Map<Code, Link>>>,
};
implement MemoryStore {
fn new() -> MemoryStore {
MemoryStore {
inner: Shared.new(RwLock.new(Map.new())),
}
}
}
implement Store for MemoryStore {
async fn insert(&self, link: Link) -> Result<(), StoreError> {
let mut w = self.inner.write().await;
match w.entry(link.code.clone()) {
MapEntry.Occupied(e) => Result.Err(StoreError.Conflict { code: e.key().clone() }),
MapEntry.Vacant(e) => { e.insert(link); Result.Ok(()) }
}
}
async fn lookup(&self, code: &Code) -> Result<Maybe<Link>, StoreError> {
let r = self.inner.read().await;
Result.Ok(r.get(code).cloned())
}
async fn increment_hits(&self, code: &Code) -> Result<(), StoreError> {
let mut w = self.inner.write().await;
match w.get_mut(code) {
Maybe.Some(link) => { link.hits += 1; Result.Ok(()) }
Maybe.None => Result.Err(StoreError.Internal(
f"link {code} disappeared".to_string())),
}
}
async fn total_count(&self) -> Result<Int, StoreError> {
let r = self.inner.read().await;
Result.Ok(r.len())
}
}
6. The HTTP layer
src/http.vr:
use core.net.http.*;
use core.text.*;
use core.time.Instant;
use .self.domain.*;
use .self.store.*;
use .self.codegen;
/// Shorten a URL — accepts JSON body `{"target": "..."}`.
pub async fn handle_shorten(body: &[Byte]) -> Result<Response, Error>
using [Store, Clock]
{
let obj = parse_json(&Text.from_utf8(body)?)?;
let raw = obj.get(&"target").and_then(Data.as_text)
.ok_or(Error.new(&"missing 'target'"))?;
let target: TargetUrl = TargetUrl.try_from(raw.clone())
.map_err(|_| Error.new(&"invalid URL"))?;
let seed = Clock.now_ns();
let code = codegen::generate(seed, 7);
let link = Link {
code: code.clone(),
target,
created: Instant.now(),
hits: 0,
};
Store.insert(link).await.map_err(|e| Error.new(&f"store: {e:?}"))?;
let body = json#"""{"code": "${code}"}""".to_bytes();
Result.Ok(Response.new(StatusCode.created())
.with_headers(Headers.new_with(&"Content-Type", &"application/json"))
.with_body(body))
}
/// Redirect handler for /s/:code
pub async fn handle_redirect(code_str: &Text) -> Result<Response, Error>
using [Store]
{
let code: Code = Code.try_from(code_str.to_string())
.map_err(|_| Error.new(&"bad code"))?;
match Store.lookup(&code).await? {
Maybe.Some(link) => {
Store.increment_hits(&code).await.ok(); // best-effort
Result.Ok(Response.new(StatusCode.new(302))
.with_headers(Headers.new_with(&"Location", &link.target))
.with_body(List.new()))
}
Maybe.None => Result.Ok(Response.new(StatusCode.not_found())),
}
}
pub async fn handle_health() -> Response using [Store] {
let count = Store.total_count().await.unwrap_or(0);
Response.new(StatusCode::ok())
.with_body(f"""{{"count": {count}}}""".to_bytes())
}
/// Route a request to the right handler.
pub async fn route(req: Request) -> Response
using [Store, Clock, Logger]
{
match (req.method, req.uri.as_str()) {
(Method.Post, "/shorten") => match handle_shorten(&req.body()).await {
Result.Ok(r) => r,
Result.Err(e) => {
Logger.warn(&f"shorten failed: {e}");
Response.new(StatusCode::bad_request())
.with_body(f"""{{"error": "{e}"}}""".to_bytes())
}
},
(Method.Get, path) if path.starts_with("/s/") => {
let code = &path[3..];
handle_redirect(&code.to_string()).await
.unwrap_or_else(|_| Response.new(StatusCode.not_found()))
}
(Method.Get, "/health") => handle_health().await,
_ => Response.new(StatusCode.not_found()),
}
}
7. The server loop — with bounded concurrency
src/main.vr:
use core.net.*;
use core.async.*;
use core.sync.Semaphore;
use .self.store_memory.MemoryStore;
use .self.http::route;
const MAX_INFLIGHT: Int = 1024; // upper bound on concurrent tasks
async fn serve() using [IO, Store, Clock, Logger] {
let listener = TcpListener.bind(&"0.0.0.0:8080").await?;
Logger.info(&"listening on :8080");
let sem = Shared.new(Semaphore.new(MAX_INFLIGHT));
nursery(on_error: wait_all) {
loop {
let (stream, peer) = listener.accept_async().await?;
let permit = sem.clone().acquire_owned().await;
spawn async move {
let _p = permit; // held for the connection
if let Result.Err(e) = serve_one(stream, peer).await {
Logger.warn(&f"{peer}: {e}");
}
};
}
}
}
async fn serve_one(mut stream: TcpStream, peer: SocketAddr) -> Result<(), Error>
using [Store, Clock, Logger]
{
let req = read_request(&mut stream).await?;
Logger.info(&f"{peer} {req.method:?} {req.uri}");
let resp = route(req).await;
write_response(&mut stream, &resp).await?;
Result.Ok(())
}
fn main() using [IO] {
let rt = Runtime.new(RuntimeConfig.default()
.worker_threads(8)
.io_engine(IoEngineKind::IoUring))
.expect("runtime");
rt.block_on(async {
provide Store = MemoryStore.new() in
provide Clock = SystemClock.new() in
provide Logger = ConsoleLogger.new(LogLevel.Info) in {
serve().await.expect("server");
}
});
}
Three things to notice:
Semaphore.new(MAX_INFLIGHT)bounds concurrent connections. Beyond 1024 in-flight,acquire().awaitsuspends — backpressure.nurseryguarantees that on shutdown every accepted connection either completes or is cancelled beforeserve()returns.provide X = v in ...layers contexts at the entry point. Every downstream function sees them viausing [...].
8. Tests
src/tests.vr:
@cfg(test)
module tests {
use .super.domain.*;
use .super.codegen;
use .super.store::*;
use .super.store_memory::MemoryStore;
use .super.http::*;
@test
fn codegen_produces_valid_code() {
let c = codegen::generate(12345, 7);
assert_eq(c.len(), 7);
assert(c.matches(rx#"^[a-hj-km-np-z2-9]+$"));
}
@test(property)
fn codegen_is_always_valid(seed: UInt64, length: Int { 6 <= self && self <= 10 }) {
let c = codegen::generate(seed, length);
assert_eq(c.len(), length);
assert(c.matches(rx#"^[a-hj-km-np-z2-9]+$"));
}
@test
async fn memory_store_round_trip() using [Clock] {
let store = MemoryStore.new();
let link = Link {
code: Code.try_from("abc123x".to_string()).unwrap(),
target: TargetUrl.try_from("https://example.com".to_string()).unwrap(),
created: Clock.now(),
hits: 0,
};
store.insert(link.clone()).await.unwrap();
let got = store.lookup(&link.code).await.unwrap().unwrap();
assert_eq(got.code, link.code);
assert_eq(got.target, link.target);
}
@test
async fn shorten_then_redirect() using [IO, Clock] {
let store = MemoryStore.new();
let logger = NullLogger.new();
provide Store = store in
provide Logger = logger in {
let body = json#"""{"target": "https://example.com"}""".to_bytes();
let resp = handle_shorten(&body).await.unwrap();
assert(resp.status.is_success());
let text = Text.from_utf8(&resp.body).unwrap();
let obj = parse_json(&text).unwrap();
let code_str = obj.get(&"code").and_then(Data.as_text).unwrap();
let r = handle_redirect(&code_str.to_string()).await.unwrap();
assert_eq(r.status.code(), 302);
let location = r.headers.get_first(&"Location").unwrap();
assert_eq(location.as_str(), "https://example.com");
}
}
}
Run:
$ verum test
[verify] codegen::generate ✓ (formal/z3, 28 ms)
test tests::codegen_produces_valid_code ... ok
test tests::codegen_is_always_valid ... ok (100 cases)
test tests::memory_store_round_trip ... ok
test tests::shorten_then_redirect ... ok
all 4 tests passed
Notice the [verify] line: the capability router dispatched to the SMT backend
and proved codegen::generate satisfies its postconditions at
compile time. The property test then sanity-checks with random
inputs.
9. Run it
$ verum run --release
listening on :8080
# In another shell
$ curl -XPOST localhost:8080/shorten -d '{"target":"https://example.com"}'
{"code":"xk8q3mr"}
$ curl -v localhost:8080/s/xk8q3mr
< HTTP/1.1 302 Found
< Location: https://example.com
$ curl localhost:8080/health
{"count":1}
10. What's next
- Persistent store: write a
PostgresStorethat implements theStoreprotocol. No other code changes. - TLS: wrap the
TcpListenerin aTlsAcceptor(see net → TLS). - Rate limiting: add a
RateLimitercontext; the sem-owned permit pattern extends naturally. - Observability: add a
Metricscontext; record request latency withInstant::elapsed().
What you learned
- Refinement types as domain boundaries.
CodeandTargetUrlreject invalid values at construction; downstream code never re-checks. - Context protocols for DI. One
Storetrait, an in-memory implement for tests, swap for production — no mocking framework. @verify(formal)with loop invariants. The code generator's post-condition is proven by Z3, not tested.- Structured concurrency with backpressure.
nurserybounds task lifetimes;Semaphorebounds total concurrency.
See also
- HTTP server cookbook — a smaller version.
- Verified data structure tutorial — deeper SMT verification.
- Nursery cookbook — structured concurrency patterns.