← Table of contents

6.2 A Port in the World

The difference between “listening” and “answering” is roughly the same as the difference between “the door is open” and “someone is home”. The first is a fact. The second is a question. A visitor with business wants the answer to the second but checks the first. The port is open.

What You Need

tq-cli answers commands. tq-api will answer HTTP requests - from any client that knows the address. Both entry points use the same store in tq-core. This chapter adds the entry point itself: a route, an address, a server.

axum is chosen for the HTTP server - a modern Rust framework built on tokio. tokio manages tasks that wait on the network without blocking a thread. This is called async.

The minimum to know right now: async fn is a function that knows how to wait; .await is “wait here”; #[tokio::main] makes async fn main() and .await inside it work. Everything else tokio handles.

The Build

The workspace gets a third crate. One new entry in the root Cargo.toml:

# Cargo.toml - CHANGED
[workspace]
members = ["crates/core", "crates/cli", "crates/api"]
resolver = "2"

Create the directory:

$ mkdir -p crates/api/src

crates/api/Cargo.toml - a binary crate with two dependencies:

# crates/api/Cargo.toml - NEW
[package]
name = "tq-api"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "tq-api"
path = "src/main.rs"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }

features = ["full"] enables the full tokio feature set. It can be tuned later; for now the details are a distraction.

The server lives in crates/api/src/main.rs:

// crates/api/src/main.rs - NEW
use axum::{routing::get, Router};

async fn health() -> String {
    "tq ok".to_string()
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(health));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Router::new().route("/", get(health)) - a route: a GET request to / is dispatched to the health function. TcpListener::bind("0.0.0.0:3000") tells the OS: incoming requests on port 3000 belong to us. A port is the number by which a client finds the right service on a machine; 3000 is simply our choice. axum::serve(listener, app) starts the request processing loop. Without .await it would never begin; with it - it runs until interrupted.

Add a new target to the Makefile:

## serve: start the HTTP server
.PHONY: serve
serve:
	@cargo run -p tq-api

The Result

Start the server in one terminal:

$ make serve

Two ways to verify it works: open http://localhost:3000/ in a browser - the page shows tq ok. Or in another terminal:

$ curl localhost:3000/
tq ok

The port is open. The request arrived, the handler answered, the connection closed - the server stayed waiting for the next. To stop it, press Ctrl+C in the terminal or close the window.

make ci passes: cargo build compiles all three crates, the tests - the same 38.

The complete tq code for this chapter is in 6-the-port/02-a-port-in-the-world/.


Lore: Async in Brief

An ordinary function returns a value or blocks the thread until it finishes. A server that blocks a thread on every request handles them one at a time - unacceptable under any real load.

async fn returns a future - a promise of a result. Tokio maintains a thread pool and decides which future to advance at each moment. .await is the point where a function says: “I am waiting - go do something else for now.”

Three rules for use:

Understanding what happens underneath - Future, Poll, wakers - is a separate study. Using it is possible right now.