typeway_server/production.rs
1//! Production deployment patterns for typeway servers.
2//!
3//! This module contains no code — it is a collection of patterns and examples
4//! for running typeway in production. Each section includes concrete, copy-pasteable
5//! code that you can adapt to your own deployment.
6//!
7//! # Health checks
8//!
9//! Every production service needs a liveness probe (`/health`) and a readiness
10//! probe (`/ready`). The liveness check confirms the process is running. The
11//! readiness check confirms the service can handle traffic (database connected,
12//! caches warm, etc.).
13//!
14//! Define them as regular typeway endpoints:
15//!
16//! ```ignore
17//! use typeway_core::*;
18//! use typeway_server::*;
19//! use std::sync::Arc;
20//! use std::sync::atomic::{AtomicBool, Ordering};
21//!
22//! // API type includes health endpoints alongside your business routes.
23//! type HealthAPI = (
24//! GetEndpoint<path!("health"), String>,
25//! GetEndpoint<path!("ready"), String>,
26//! );
27//!
28//! // Liveness: always returns 200 if the process is alive.
29//! async fn health() -> String {
30//! "ok".to_string()
31//! }
32//!
33//! // Readiness: checks dependencies before reporting ready.
34//! async fn ready(State(state): State<AppState>) -> (http::StatusCode, String) {
35//! if state.is_ready.load(Ordering::Relaxed) {
36//! (http::StatusCode::OK, "ready".to_string())
37//! } else {
38//! (http::StatusCode::SERVICE_UNAVAILABLE, "not ready".to_string())
39//! }
40//! }
41//!
42//! #[derive(Clone)]
43//! struct AppState {
44//! is_ready: Arc<AtomicBool>,
45//! }
46//! ```
47//!
48//! Kubernetes probe configuration for the above:
49//!
50//! ```yaml
51//! livenessProbe:
52//! httpGet:
53//! path: /health
54//! port: 3000
55//! initialDelaySeconds: 2
56//! periodSeconds: 10
57//! readinessProbe:
58//! httpGet:
59//! path: /ready
60//! port: 3000
61//! initialDelaySeconds: 5
62//! periodSeconds: 5
63//! ```
64//!
65//! # Graceful shutdown
66//!
67//! Use [`Server::serve_with_shutdown`](crate::Server::serve_with_shutdown) to
68//! stop accepting new connections when a shutdown signal arrives. In-flight
69//! requests on existing connections are allowed to complete. New TCP connections
70//! are refused immediately.
71//!
72//! ```ignore
73//! use typeway_server::Server;
74//! use tokio::net::TcpListener;
75//!
76//! let server = Server::<API>::new(handlers);
77//! let listener = TcpListener::bind("0.0.0.0:3000").await?;
78//!
79//! server.serve_with_shutdown(listener, async {
80//! tokio::signal::ctrl_c().await.ok();
81//! println!("Received Ctrl+C, starting shutdown...");
82//! }).await?;
83//! ```
84//!
85//! What happens during shutdown:
86//!
87//! 1. The shutdown future completes (e.g., `ctrl_c()` fires).
88//! 2. The accept loop exits — no new TCP connections are accepted.
89//! 3. Already-spawned connection tasks continue running until their
90//! current request/response cycle finishes.
91//! 4. Once all spawned tasks complete, the process exits cleanly.
92//!
93//! If you need a hard deadline on in-flight requests, wrap the serve call
94//! with [`tokio::time::timeout`]:
95//!
96//! ```ignore
97//! use std::time::Duration;
98//!
99//! let result = tokio::time::timeout(
100//! Duration::from_secs(30),
101//! server.serve_with_shutdown(listener, async {
102//! tokio::signal::ctrl_c().await.ok();
103//! }),
104//! ).await;
105//!
106//! match result {
107//! Ok(Ok(())) => println!("Clean shutdown"),
108//! Ok(Err(e)) => eprintln!("Server error: {e}"),
109//! Err(_) => eprintln!("Shutdown timed out after 30s, forcing exit"),
110//! }
111//! ```
112//!
113//! # Load balancer draining
114//!
115//! When deploying behind a load balancer (ALB, NLB, HAProxy, envoy, etc.),
116//! you want to drain traffic before shutting down. The pattern:
117//!
118//! 1. Receive SIGTERM (or other shutdown signal).
119//! 2. Set readiness to `false` so the load balancer stops sending new requests.
120//! 3. Wait a drain period for the LB to detect the change and reroute traffic.
121//! 4. Shut down the server, letting in-flight requests finish.
122//!
123//! ```ignore
124//! use std::sync::Arc;
125//! use std::sync::atomic::{AtomicBool, Ordering};
126//! use std::time::Duration;
127//! use typeway_server::Server;
128//! use tokio::net::TcpListener;
129//!
130//! #[derive(Clone)]
131//! struct AppState {
132//! is_ready: Arc<AtomicBool>,
133//! }
134//!
135//! async fn ready(State(state): State<AppState>) -> (http::StatusCode, String) {
136//! if state.is_ready.load(Ordering::Relaxed) {
137//! (http::StatusCode::OK, "ready".to_string())
138//! } else {
139//! (http::StatusCode::SERVICE_UNAVAILABLE, "draining".to_string())
140//! }
141//! }
142//!
143//! #[tokio::main]
144//! async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
145//! let state = AppState {
146//! is_ready: Arc::new(AtomicBool::new(true)),
147//! };
148//! let is_ready = state.is_ready.clone();
149//!
150//! let server = Server::<API>::new((health, ready, /* ...other handlers... */))
151//! .with_state(state);
152//!
153//! let listener = TcpListener::bind("0.0.0.0:3000").await?;
154//!
155//! server.serve_with_shutdown(listener, async move {
156//! // Wait for SIGTERM (container orchestrators send this).
157//! tokio::signal::ctrl_c().await.ok();
158//!
159//! // Step 1: Mark as not ready so the LB stops routing to us.
160//! is_ready.store(false, Ordering::Relaxed);
161//! tracing::info!("Marked as not ready, draining for 15 seconds...");
162//!
163//! // Step 2: Wait for the load balancer to notice and drain.
164//! // This should be >= your LB's health check interval.
165//! tokio::time::sleep(Duration::from_secs(15)).await;
166//!
167//! tracing::info!("Drain period complete, shutting down.");
168//! // Returning from this future triggers the actual shutdown.
169//! }).await
170//! }
171//! ```
172//!
173//! Tune the drain period to match your load balancer's health check interval.
174//! For AWS ALB with a 10-second check interval, 15 seconds is a safe drain
175//! period. For Kubernetes with a 5-second readiness probe, 10 seconds suffices.
176//!
177//! # Recommended middleware stack
178//!
179//! The order of middleware layers matters. Layers are applied outside-in: the
180//! first `.layer()` call wraps the outermost layer. Here is a recommended
181//! production stack:
182//!
183//! ```ignore
184//! use typeway_server::{Server, SecureHeadersLayer};
185//! use tower_http::trace::TraceLayer;
186//! use tower_http::cors::CorsLayer;
187//! use tower_http::timeout::TimeoutLayer;
188//! use tower_http::compression::CompressionLayer;
189//! use std::time::Duration;
190//!
191//! let server = Server::<API>::new(handlers)
192//! .with_state(state)
193//! // 1. SecureHeadersLayer (outermost): adds security headers to every
194//! // response — X-Content-Type-Options, X-Frame-Options, etc.
195//! // Applied first so that even error responses get security headers.
196//! .layer(SecureHeadersLayer::new())
197//! // 2. TraceLayer: logs every request/response with timing info.
198//! // Outside of CORS so preflight requests are also logged.
199//! .layer(TraceLayer::new_for_http())
200//! // 3. CorsLayer: handles preflight OPTIONS requests and sets
201//! // Access-Control-* headers. Must be outside the timeout layer
202//! // so preflight responses are not subject to handler timeouts.
203//! .layer(CorsLayer::permissive())
204//! // 4. TimeoutLayer: returns 408 Request Timeout if a handler takes
205//! // too long. Only applies to actual handler execution, not to
206//! // preflight or middleware processing above.
207//! .layer(TimeoutLayer::new(Duration::from_secs(30)))
208//! // 5. CompressionLayer (innermost): compresses response bodies.
209//! // Inside timeout so that compression time counts toward the
210//! // timeout budget.
211//! .layer(CompressionLayer::new());
212//!
213//! server.serve("0.0.0.0:3000".parse().unwrap()).await?;
214//! ```
215//!
216//! Adjust to your needs:
217//!
218//! - **CORS**: Replace `CorsLayer::permissive()` with a restrictive policy
219//! for production. Specify allowed origins, methods, and headers explicitly.
220//! - **Timeout**: 30 seconds is a reasonable default. Lower it for APIs with
221//! strict latency SLOs.
222//! - **Compression**: If your responses are already compressed (e.g., pre-gzipped
223//! static files), you can omit this or move it outside the timeout layer.
224//!
225//! # Panic recovery
226//!
227//! Typeway catches panics in request handlers and converts them to 500 Internal
228//! Server Error responses. A panicking handler does not take down the server
229//! process — only the individual request fails.
230//!
231//! The panic message is logged via `tracing::error!` for debugging, but is not
232//! exposed to the client (to avoid leaking internal details). The client
233//! receives a generic 500 response.
234//!
235//! This means:
236//!
237//! - You do not need a separate `CatchPanic` middleware in most cases.
238//! - Individual handler bugs are isolated to the request that triggered them.
239//! - The server continues accepting and processing other requests normally.
240//! - You should still fix panics — they indicate bugs — but they will not
241//! cause cascading failures or downtime.
242//!
243//! If you use [`std::panic::set_hook`] for custom panic reporting (e.g.,
244//! sending to Sentry), it will fire for handler panics as well, giving you
245//! full stack traces alongside the typeway error log.