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
tqcode for this chapter is in6-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:
async fn foo()- a function that knows how to waitfoo().await- wait for the result#[tokio::main]- makesasync fn main()and.awaitinside it work
Understanding what happens underneath - Future, Poll, wakers - is a separate study.
Using it is possible right now.