Quick Start
Four paths to a running rust-web-server in under five minutes — static files, a first route, a config-driven proxy, and automatic HTTPS.
Pick the path that matches your goal. Each ends with a working server you can hit with curl.
Path 1 — Static file server (zero code)
Install the binary and point it at a directory. No config file, no code.
brew install bohdaq/tap/rws # or: cargo binstall rust-web-server
# create something to serve
mkdir www && echo "<h1>Hello</h1>" > www/index.html
cd www
rws
Server is up and running at: http://0.0.0.0:7878
curl http://localhost:7878/
# <h1>Hello</h1>rws serves every file under the current directory, handles range requests, sets ETags, negotiates gzip, and returns 404 for missing files — all with no configuration. A directory with no index.html renders a directory listing page instead of 404 — try mkdir www/uploads && touch www/uploads/report.pdf && curl http://localhost:7878/uploads/.
If you have a TLS certificate, pass --tls-cert-file=cert.pem --tls-key-file=key.pem and the server automatically upgrades to HTTP/2 + HTTP/3:
rws --tls-cert-file=cert.pem --tls-key-file=key.pemPath 2 — First route (library)
Use rust-web-server as a Rust library crate and define your own handlers.
1. Add dependencies
[dependencies]
rust-web-server = "18"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }2. Write a handler
use rust_web_server::prelude::*;
fn hello(_req: &Request, _params: &PathParams, _conn: &ConnectionInfo, _state: &()) -> Response {
Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "Hello, world!")
}
#[tokio::main]
async fn main() {
let app = routes! {
App::with_state(()),
GET "/hello" => hello,
};
let (listener, pool) = Server::setup().unwrap();
tokio::join!(
Server::run_tls(listener, pool, app.clone()),
Server::run_quic(app),
Server::run_redirect(),
);
}3. Run and verify
cargo run
curl http://localhost:7878/hello
# Hello, world!Handler signature
Every route handler receives four arguments:
| Argument | Type | What it contains |
|---|---|---|
_req |
&Request |
Method, URI, headers, raw body bytes |
_params |
&PathParams |
Named path segments (e.g. :id) |
_conn |
&ConnectionInfo |
Client/server IP, port, SNI hostname |
_state |
&S |
Your shared application state |
Shared state
Replace () with any Send + Sync type to share state across handlers:
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
struct AppState {
counter: AtomicU64,
}
fn count(_: &Request, _: &PathParams, _: &ConnectionInfo, state: &Arc<AppState>) -> Response {
let n = state.counter.fetch_add(1, Ordering::Relaxed);
Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, &format!("count: {n}"))
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState { counter: AtomicU64::new(0) });
let app = routes! {
App::with_state(state),
GET "/count" => count,
};
let (listener, pool) = Server::setup().unwrap();
tokio::join!(
Server::run_tls(listener, pool, app.clone()),
Server::run_quic(app),
Server::run_redirect(),
);
}Path 3 — Config-driven proxy (no code)
Drop an rws.config.toml next to the rws binary and it becomes a reverse proxy and load balancer. No code required.
Minimal config
port = 8080
[[upstream]]
name = "api"
backends = ["localhost:3000", "localhost:3001"]
[upstream.health_check]
path = "/healthz"
interval_secs = 10
healthy_threshold = 2
[[route]]
[route.match]
path = "/api/*"
[route.action]
type = "proxy"
upstream = "api"
[[route]]
[route.match]
path = "/"
[route.action]
type = "respond"
status = 200
body = "Gateway ready"
rws
# Server is up and running at: http://0.0.0.0:8080
curl http://localhost:8080/
# Gateway ready
curl http://localhost:8080/api/users
# proxied to localhost:3000 or localhost:3001 (round-robin, health-checked)What the config does
[[upstream]]declares a named backend pool with health checking. Dead backends are removed automatically; they re-enter rotation once they passhealthy_thresholdconsecutive checks.[[route]]rules are evaluated top-to-bottom, first match wins. Apathending in*is a prefix match (/api/*matches/api/users); without it the path must match exactly. Requests no route matches fall through to the static-file server.type = "proxy"forwards the request to the named upstream.type = "respond"returns a fixed response.
Rate limiting, Bearer auth, and request rewriting can all be added per-route inside the config file — no code change needed. See the Proxy & Gateway documentation for the full schema.
Add an [acme] section (Path 4) and rws gets Let's Encrypt certificates for the proxy itself. To use your own certificates instead, set tls_cert_file / tls_key_file; per-domain certificates go in [[virtual_host]] blocks.
Path 4 — Automatic HTTPS (no code)
On a server whose DNS points at it, with ports 80 and 443 reachable:
port = 443
http_redirect_port = 80
[acme]
domains = ["example.com"]
email = "you@example.com"
sudo rws # or grant the binary CAP_NET_BIND_SERVICE to bind 80/443 without rootOn first start rws requests a certificate from Let's Encrypt (stored as
cert.pem / key.pem), then serves HTTPS with HTTP/2 and HTTP/3 and
redirects HTTP to HTTPS. Renewal runs in the background. Try it against the
Let's Encrypt staging CA first with staging = true — see
Automatic HTTPS.
curl -I http://example.com/ # 301 → https://example.com/
curl -I https://example.com/ # 200, valid certificateNext steps
- Dev Workflow — auto-rebuild and restart on every code change with
cargo-watch - Routing — named path parameters (
:id), wildcards, virtual-host routing - Middleware —
RateLimitLayer,JwtLayer,CacheLayer,OtelLayer, and more via.wrap(layer) - MCP Server — expose tools and resources to AI agents over the MCP Streamable HTTP protocol
- ORM —
#[derive(Model)],QueryBuilder, migrations,HasMany/HasOne/BelongsTo