This episode builds your first HTTP server in Rust with axum, discusses request-response, routing, middleware, and error handling, then introduces basic TCP and UDP connections as well as the service discovery concept for distributed applications.

After mastering data and configuration, it is time for your Rust application to talk to the outside world. Episode 10 is the point where your program turns into a real service: receiving HTTP requests, processing them, and returning responses.
We start with the networking basics in std::net, then build an HTTP server with axum — a modern framework built on hyper. You will learn routing, middleware, error handling, and get a look at warp, actix-web, and the concept of service discovery. By the end of the episode, you will have an API you can test with curl.
Before HTTP, understand its transport: TCP provides a reliable, ordered connection, while UDP provides fast datagrams without guarantees. The standard library provides both:
cat > src/main.rs <<'EOF'
use std::io::{Read, Write};
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:8000").unwrap();
println!("mendengarkan di 127.0.0.1:8000");
for stream in listener.incoming() {
let mut stream = stream.unwrap();
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let respons = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
stream.write_all(respons).unwrap();
}
}
EOF
cargo runTcpListener::bind("127.0.0.1:8000") opens a local port, and listener.incoming() yields incoming connections. The example above even responds over HTTP — although very primitively. Run cargo run, then test with curl http://127.0.0.1:8000 from another terminal.
UDP is used for DNS, telemetry, and games because its overhead is minimal. In Rust, UdpSocket::bind and send_to/recv_from handle datagrams. For HTTP and business services, TCP remains the primary choice.
axum is built on hyper and tower, using an expressive handler and extractor architecture. Add the dependencies with cargo add axum tokio --features tokio/full:
cat > src/main.rs <<'EOF'
use axum::{routing::get, Router};
async fn halo() -> &'static str {
"Halo dari axum"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(halo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("listening on 127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
EOF
cargo runRouter::new().route("/", get(halo)) registers a handler for a path. axum::serve runs the async server. Test with curl http://127.0.0.1:3000 and you will see the Halo dari axum response.
Handlers accept extractors such as Path, Query, Json, and State, and return any type that implements IntoResponse:
cat > src/main.rs <<'EOF'
use axum::{extract::Path, routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct Pengguna {
id: u32,
nama: String,
}
async fn detail(Path(id): Path<u32>) -> Json<Pengguna> {
Json(Pengguna {
id,
nama: format!("pengguna {}", id),
})
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/api/pengguna/{id}", get(detail));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
EOF
cargo runPath(id): Path<u32> extracts the value from the {id} segment. Json<Pengguna> creates a JSON response from a struct implementing Serialize. Test with curl http://127.0.0.1:3000/api/pengguna/7.
Middleware handles cross-cutting request concerns: logging, timeout, CORS, and compression. axum uses layers from tower:
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/", get(|| async { "selamat datang" }))
.layer(TraceLayer::new_for_http())
.fallback(|| async { (StatusCode::NOT_FOUND, "tidak ditemukan") });TraceLayer logs every request along with its status and duration. fallback handles paths that are not registered. Layers are attached with .layer(...) and applied from outside in.
Handlers return Result<T, E> where E: IntoResponse:
struct AppError(anyhow::Error);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("terjadi kesalahan: {}", self.0),
)
}
}
async fn index() -> Result<&'static str, AppError> {
Ok("api sehat")
}Errors from handlers are converted into HTTP responses with the appropriate status. The ? operator inside a handler propagates internal errors, and AppError turns them into a client-friendly response.
Beyond axum: warp uses composable filter combinators, actix-web offers an actor model and high performance, while hyper is the low-level HTTP foundation that axum itself uses. Choose axum for the tokio ecosystem, actix-web for teams that want an all-in-one framework, and warp for a functional style.
In distributed environments, service instances do not know each other's addresses. Service discovery solves this: instances register themselves in a registry (for example etcd, Consul, or Kubernetes' built-in registry), and consumers query the registry to find active addresses. In Rust, crates such as rust-consul and tonic (for gRPC) are the bridge to this ecosystem.
Key takeaways:
std::net provides basic TCP and UDP; TCP for reliable services.Path, Query, and Json shape an API.In the next episode 11 we will discuss security, TLS, and auth — enabling HTTPS with TLS and certificates, implementing JWT, OAuth2, and session authentication, as well as input protection, CORS, rate limiting, and security headers. Your API is ready to be exposed to the public.