# ntex > ntex is a Rust framework for composable network services: HTTP/1, HTTP/2, WebSocket, web applications, and custom protocols. Everything is a `Service` with a pipeline state type `St`, executed by a `Pipeline` that coordinates readiness, calls, and shutdown. Rules for generated code: - A service implements `call(&self, req, ctx: Ctx<'_, Self, St>)`. Call child services with `ctx.call(&self.child, req)`, never `self.child.call(...)` directly. - A service that wraps child services must forward `bind`, `ready`, and `shutdown` to every child. Use `ntex::forward!(St, field1, field2)`; add `map_err = f` when `Self::Error` differs from the children's error. `St` is the state type parameter of the impl. - For web middleware that only runs code before or after the inner service, use `ntex::web::middleware::from_fn(async |req, next| ...)` and call `next.call(req).await?`. No annotations are needed, except `next: Next<'_, _, MyState>` when the closure reads `next.st()`. Return `Err(web::WebError::from_err(err))` to reject a request. - For non-web services use `ntex::service::fn_layer(async |req: Req, ctx| ...)` with explicit request, response, and error types. - Type-inference rules for service closures: always name the error type in `fn_service`, `fn_factory`, and `fn_layer` results (`Ok::<_, MyError>(..)`); annotate the argument of closures passed to `and_then`/`then` (`.and_then(async |res: Res| ...)`), or wrap them in `fn_service`. Closures for `map`, `map_err`, `map_init_err`, and `apply_fn` need no annotations. - Type-inference rules for web code: inside `HttpServer::new` or `test::init_service` write `App::new()`; elsewhere write `App::default()` (state `()`) or `App::>::new()`. Prefer `async fn` handlers; closure handlers must annotate every argument (`async |path: web::types::Path| ...`) and name the error type of `Ok(..)` results, also in `filter` closures. `?` does not convert into `WebError`; return a concrete error type that implements `WebResponseError`, or use `.map_err(web::WebError::from_err)?`. Modular config functions take `cfg: &mut web::ServiceConfig` (state `()`) or `&mut web::ServiceConfig`. - Use a `Middleware` struct plus a service with `forward!` when the middleware needs its own configuration, readiness, or a named service type. - Write `ctx.bind(&self.child)` without a turbofish. - Implement `bind`, `ready`, and `shutdown` by hand only when the service has extra readiness sources, such as a shared `Pipeline` (`pipeline.bind_to(&ctx)`) or a `ReadyNotifier` (`ctx.subscribe(¬ifier)`). - Async entry points use `#[ntex::main]`; async tests use `#[ntex::test]`. ## Server and client rules - For HTTP applications, start with `web::HttpServer` and `web::App`. For custom TCP protocols or lower-level HTTP services, use `ntex::server::build()`; the standalone crate exposes the same builder as `ntex_server::net::build()`. - Build servers inside an active ntex runtime. `server::build().bind(name, addr, SharedCfg::default(), async |state| service)` takes four arguments; `web::HttpServer::new(async |state| app).bind(addr, SharedCfg::default())` takes two. Factories are asynchronous even when construction does no asynchronous work. Call `.run().await` to wait for shutdown, not to check worker health. - Factories run per worker and may run again after a restart. Construct `Rc`/`Cell`/`RefCell` state inside `build_with_config(async || Ok(state))`, not outside a factory that must be `Send`. Use `Arc` plus appropriate synchronization when data must be shared across workers. `SharedCfg` configures I/O and protocols; it is not application state. - `HttpService::new(async |req| Ok(..))` needs an explicit error type (`Ok::<_, std::io::Error>(..)`). A named `async fn` with a concrete return type avoids this annotation. Finish response builders with `.build()`, not `.finish()`. - For simple sequential protocols use `Io::recv(&codec)` and `Io::send(item, &codec)`. `BytesCodec` yields available byte chunks, not application-message boundaries. Use a framing codec for message protocols. - `Dispatcher::new(io, codec, pipeline)` requires a `Pipeline` whose service accepts `DispatchItem` and returns `Result, E>`. Match `Item` separately from `Control`/`Stop` notifications; `Ok(None)` emits no response. - Service factories for `server::test_server`, `http::test::server`, and `web::test::server` take `async |_| service_or_app`, with `&()` as the argument. `server::TestServerBuilder::with(cfg, factory)` passes `&Cfg::State` to the service factory. State-initialization closures remain zero-argument: `async || Ok(state)`. Use the test server's address, not a fixed port. - Client send/body errors do not automatically convert into `std::io::Error`. Use a concrete application error or `Result<_, Box>` to preserve them with `?`. If only a message is needed, explicitly use `.map_err(|err| std::io::Error::other(err.to_string()))?`; this loses the original error and diagnostic context. Do not require `Send + Sync` on the boxed error: client errors can be worker-local. - TLS belongs in connection setup, not protocol logic. Compose acceptors/connectors with services over `Io`; use generic filters, `seal()`, or `boxed()` to keep protocol code independent of TLS backends. - HTTP clients use `openssl`, `rustls`, or `secure_connector` for HTTPS; `ClientBuilder::connector` configures unsecured connections. Supply verifying TLS configurations explicitly: the built-in OpenSSL HTTP-client default currently disables certificate verification. - `WsClient` needs an explicit TLS connector even for `wss://`: use `openssl`, `rustls`, or `connector`, and offer HTTP/1.1 rather than h2. Server-side WebSocket handlers work unchanged behind an HTTPS listener. - Read peer certificates with `PeerCertDer` / `PeerCertChainDer`, then parse with `x509::PeerCerts`. Certificate presence or parsing is not trust verification or authorization. Schannel's `request_client_cert(true)` neither requires a certificate nor validates its trust; reject missing/untrusted identities before protected handling. - See the [TLS guide](https://docs.ntex.rs/10-tls.html) and [runnable TLS examples](https://github.com/ntex-rs/examples/tree/main/tls) for backend, component, identity, and mutual-authentication configuration. ## Testing rules - Test the application the server runs. Register routes in `fn config(cfg: &mut web::ServiceConfig)` (or `&mut web::ServiceConfig`), call `App::new().configure(config)` in both `main` and the tests, and keep tests in `#[cfg(test)] mod tests` at the bottom of the file. - Prefer in-process tests: `web::test::init_service(App::new()...)` with `TestRequest` and `call_service` opens no socket. Use `init_service_st(state.clone(), App::::new()...)` for stateful apps and keep a clone of shared handles to inspect state. - Use a real server (`web::test::server`, `http::test::server`, `server::test_server`) only for WebSocket, streaming, client, TLS, or connection behavior; it binds an available port. Never use fixed ports, external services, or sleeps for synchronization. - Test a `Service` directly with `Pipeline::new(state, service).call(req).await`. Test codecs and `Io` protocols in memory with `ntex::io::testing::IoTest`. - Assert the status and the body; for failures, assert the status code a client sees. Use `read_body` for bytes and `read_response_json` for JSON. ## Templates Function service executed by a pipeline: ```rust use ntex::service::{Pipeline, fn_service}; let service = fn_service(async |req: u32| Ok::<_, std::io::Error>(req + 1)); let pipeline = Pipeline::new((), service); assert_eq!(pipeline.call(1).await?, 2); ``` Minimal web server: ```rust use ntex::{SharedCfg, web}; #[web::get("/")] async fn index() -> &'static str { "Hello world!" } #[ntex::main] async fn main() -> std::io::Result<()> { web::HttpServer::new(async |_| web::App::new().service(index)) .bind("127.0.0.1:8080", SharedCfg::new("hello-world"))? .run() .await } ``` Closure web middleware (preferred): ```rust use ntex::http::header::{HeaderName, HeaderValue}; use ntex::web::{self, App, HttpResponse, middleware::from_fn}; web::HttpServer::new(async |_| { App::new() .middleware(from_fn(async |req, next| { if !req.headers().contains_key("authorization") { return Ok(req.into_response(HttpResponse::Unauthorized().build())); } let mut res = next.call(req).await?; res.headers_mut().insert( HeaderName::from_static("x-application"), HeaderValue::from_static("example"), ); Ok(res) })) .route("/", web::get().to(async || "Hello")) }); ``` Struct middleware with forwarding: ```rust use std::time::Instant; use ntex::web::{WebRequest, WebResponse}; use ntex::{Ctx, Middleware, Service}; pub struct Timing; pub struct TimingService { service: S, } impl Middleware for Timing { type Service = TimingService; fn create(&self, _: &St, service: S) -> Self::Service { TimingService { service } } } impl Service> for TimingService where S: Service, Res = WebResponse>, { type Res = WebResponse; type Error = S::Error; ntex::forward!(St, service); async fn call( &self, req: WebRequest, ctx: Ctx<'_, Self, St>, ) -> Result { let start = Instant::now(); let res = ctx.call(&self.service, req).await; log::info!("request took {:?}", start.elapsed()); res } } ``` Several children with error conversion: ```rust use ntex::{Ctx, Service}; pub enum AuthError { Denied, Service(E), } pub struct Authorized { check: A, service: S, } impl Service for Authorized where A: Service, S: Service, { type Res = S::Res; type Error = AuthError; ntex::forward!(St, check, service, map_err = AuthError::Service); async fn call(&self, req: Req, ctx: Ctx<'_, Self, St>) -> Result { let (req, allowed) = ctx.call(&self.check, req).await.map_err(AuthError::Service)?; if !allowed { return Err(AuthError::Denied); } ctx.call(&self.service, req).await.map_err(AuthError::Service) } } ``` Explicit forwarding with a shared pipeline child: ```rust use ntex::service::{BindCtx, Ctx, Pipeline, Service}; pub struct Cached { service: S, // shared with other services, owned and shut down elsewhere cache: Pipeline, E>, } impl Service for Cached where S: Service, E: 'static, { type Res = String; type Error = E; fn bind(&self, ctx: BindCtx<'_>) { ctx.bind(&self.service); self.cache.bind_to(&ctx); } async fn ready(&self, ctx: Ctx<'_, Self, St>) -> Result<(), E> { self.cache.ready().await?; ctx.ready(&self.service).await } async fn shutdown(&self, ctx: Ctx<'_, Self, St>) { // the shared cache pipeline is shut down by its owner ctx.shutdown(&self.service).await; } async fn call(&self, req: String, ctx: Ctx<'_, Self, St>) -> Result { if let Some(res) = self.cache.call(req.clone()).await? { return Ok(res); } ctx.call(&self.service, req).await } } ``` TCP byte-echo server: ```rust use ntex::{SharedCfg, codec::BytesCodec, io::Io, service::fn_service}; async fn echo(io: Io) -> std::io::Result<()> { while let Some(msg) = io.recv(&BytesCodec).await? { io.send(msg, &BytesCodec).await?; } Ok(()) } #[ntex::main] async fn main() -> std::io::Result<()> { ntex::server::build() .bind("echo", "127.0.0.1:8080", SharedCfg::default(), async |_| fn_service(echo))? .run() .await } ``` Dispatcher-based byte-echo server (control and stop notifications need no response here): ```rust use ntex::codec::BytesCodec; use ntex::io::{DispatchItem, Dispatcher, Io}; use ntex::service::{Pipeline, fn_service}; async fn serve(io: Io) -> std::io::Result<()> { let handler = fn_service(async |item: DispatchItem| match item { DispatchItem::Item(frame) => Ok::<_, std::io::Error>(Some(frame)), _ => Ok(None), }); Dispatcher::new(io, BytesCodec, Pipeline::new((), handler)).await } #[ntex::main] async fn main() -> std::io::Result<()> { ntex::server::build() .bind("echo", "127.0.0.1:8080", ntex::SharedCfg::default(), async |_| fn_service(serve))? .run() .await } ``` Low-level HTTP server without `web::App`: ```rust use ntex::SharedCfg; use ntex::http::{HttpService, Request, Response}; async fn handle(req: Request) -> Result { Ok(Response::Ok().body(format!("path: {}", req.path()))) } #[ntex::main] async fn main() -> std::io::Result<()> { ntex::server::build() .bind("http", "127.0.0.1:8080", SharedCfg::default(), async |_| HttpService::new(handle))? .run() .await } ``` Network test with `test_server` and `Client`: ```rust use ntex::client::Client; use ntex::http::{HttpService, Response}; #[ntex::test] async fn http_roundtrip() -> Result<(), Box> { let srv = ntex::server::test_server(async |_| { HttpService::new(async |_| Ok::<_, std::io::Error>(Response::Ok().body("hello"))) }); let url = format!("http://{}/", srv.addr()); let res = Client::new().get(url.as_str()).send().await?; assert!(res.status().is_success()); assert_eq!(res.body().await?, "hello"); Ok(()) } ``` In-process route test that uses the application's config: ```rust use ntex::http::StatusCode; use ntex::web::{self, App, HttpResponse, test}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Serialize, Deserialize)] struct User { name: String, } async fn create(user: web::types::Json) -> HttpResponse { HttpResponse::Created().json(&user.into_inner()) } pub fn config(cfg: &mut web::ServiceConfig) { cfg.route("/users", web::post().to(create)); } #[cfg(test)] mod tests { use super::*; #[ntex::test] async fn create_user() { let app = test::init_service(App::new().configure(config)).await; let user = User { name: "alice".into() }; let req = test::TestRequest::post().uri("/users").json(&user).to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::CREATED); let body: User = serde_json::from_slice(&test::read_body(res).await).unwrap(); assert_eq!(body, user); let req = test::TestRequest::post().uri("/users").header("content-type", "application/json").payload("{").to_request(); let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); } } ``` Stateful route test (keep a clone of the shared handle to inspect state): ```rust use std::{cell::Cell, rc::Rc}; use ntex::web::{self, App, AppState, test}; type Hits = AppState>>; async fn hit(hits: &Hits, (): ()) -> String { hits.set(hits.get() + 1); hits.get().to_string() } pub fn config(cfg: &mut web::ServiceConfig) { cfg.route("/hit", web::post().to_with_state(hit)); } #[ntex::test] async fn counts_hits() { let hits = Hits::default(); let app = test::init_service_st(hits.clone(), App::::new().configure(config)).await; for expected in ["1", "2"] { let req = test::TestRequest::post().uri("/hit").to_request(); let res = test::call_service(&app, req).await; assert!(res.status().is_success()); assert_eq!(test::read_body(res).await, expected); } assert_eq!(hits.get(), 2); } ``` WebSocket test against a real test server: ```rust use ntex::util::Bytes; use ntex::web::{self, App, HttpRequest, test, ws}; async fn echo(frame: ws::Frame) -> Result, std::io::Error> { Ok(match frame { ws::Frame::Text(text) => Some(ws::Message::Text(String::from_utf8_lossy(&text).as_ref().into())), ws::Frame::Ping(msg) => Some(ws::Message::Pong(msg)), ws::Frame::Close(reason) => Some(ws::Message::Close(reason)), _ => None, }) } async fn socket(req: HttpRequest) { let _ = ws::start(&req, None, echo).await; } #[ntex::test] async fn ws_echo() -> Result<(), Box> { let srv = test::server(async |_| App::new().route("/ws", web::get().to(socket))); let (io, codec, _) = srv.ws_at("/ws").await?.into_inner(); io.send(ws::Message::Text("hello".into()), &codec).await?; assert_eq!(io.recv(&codec).await?, Some(ws::Frame::Text(Bytes::from_static(b"hello")))); Ok(()) } ``` Protocol test without sockets, for `echo` from the TCP byte-echo template: ```rust use ntex::io::testing::IoTest; #[ntex::test] async fn echo_in_memory() { let (client, server) = IoTest::create(); client.remote_buffer_cap(1024); let _task = ntex::rt::spawn(echo(Io::from(server))); client.write(b"ping"); assert_eq!(client.read().await.unwrap(), b"ping"[..]); } ``` Worker-local state (counts accepted connections, then closes them): ```rust use std::{cell::Cell, rc::Rc}; use ntex::{SharedCfg, io::Io}; #[derive(Clone, Default)] struct Connections(Rc>); async fn handle(st: &Connections, _io: Io) -> std::io::Result<()> { st.0.set(st.0.get() + 1); Ok(()) } #[ntex::main] async fn main() -> std::io::Result<()> { ntex::server::build_with_config(async || Ok(Connections::default())) .bind("svc", "127.0.0.1:8080", SharedCfg::default(), async |_| ntex::service(handle))? .run() .await } ``` ## Coming from actix-web | actix-web | ntex | |---|---| | `#[actix_web::main]`, `#[actix_web::test]` | `#[ntex::main]`, `#[ntex::test]` | | `HttpServer::new(\|\| App::new())` | `web::HttpServer::new(async \|_\| App::new())` or `web::server(..)` | | `.bind(("127.0.0.1", 8080))?` | `.bind("127.0.0.1:8080", SharedCfg::default())?` | | `.wrap(m)` | `.middleware(m)` on `App`, `Scope`, `Resource` | | `middleware::from_fn`, `wrap_fn` | `web::middleware::from_fn(async \|req, next\| next.call(req).await)` | | `middleware::{Logger, DefaultHeaders, Compress}` | same names in `web::middleware` (`Compress` needs feature `compress`) | | `web::Data` + `.app_data(..)` | `web::AppState` from `web::server_with_config(async \|\| Ok(AppState::new(v)), ..)`, read by handlers registered with `.to_with_state(h)` where `async fn h(st: &AppState, (): (), ..)` | | `web::Json/Path/Query/Form/Payload` | `web::types::Json/Path/Query/Form/Payload` | | `Bytes`, `Either` | `ntex::util::Bytes`, `ntex::util::Either` | | `ResponseError` | `web::WebResponseError` with `fn error_response(&self, st: &St) -> HttpResponse`; type must impl `std::error::Error` | | `actix_web::Error` | `web::WebError`; `?` does not convert, use `.map_err(web::WebError::from_err)?` | | `HttpResponse::Ok().finish()` | `HttpResponse::Ok().build()`; `.json(&value)` takes a reference | | `ServiceRequest`, `ServiceResponse` | `web::WebRequest`, `web::WebResponse` | | `#[get("/")]` | `#[web::get("/")]` | | `actix_web::rt::spawn` | `ntex::rt::spawn` | | `actix_web::test::*` | `ntex::web::test::{init_service, call_service, read_body, TestRequest}` | `scope`, `resource`, `route`, `service`, `to`, `configure`, `ServiceConfig` and `default_service` keep their actix-web names. ## Guide - [Service Model](https://docs.ntex.rs/1-service.html): the `Service` trait, factories, and function services - [Service Pipelines](https://docs.ntex.rs/2-pipeline.html): readiness, binding, forwarding, and shutdown - [Service State](https://docs.ntex.rs/3-state.html) - [Runtime](https://docs.ntex.rs/4-runtime.html) - [Server](https://docs.ntex.rs/5-server.html) - [Worker and Request State](https://docs.ntex.rs/6-server-app.html) - [I/O Abstraction Layer](https://docs.ntex.rs/7-io.html) - [Applications and Routing](https://docs.ntex.rs/8.1-web-app.html) - [Application State](https://docs.ntex.rs/8.2-web-app.html) - [Filters and Middleware](https://docs.ntex.rs/8.3-web-app.html) - [Extractors and Responders](https://docs.ntex.rs/8.4-web-app.html) - [Error Handling](https://docs.ntex.rs/8.5-web-app.html) - [WebSocket Support](https://docs.ntex.rs/8.6-web-app.html) - [Byte Buffers](https://docs.ntex.rs/9-bytes.html) - [Securing Connections with TLS](https://docs.ntex.rs/10-tls.html): backend and component configuration, identities, peer certificates, and mutual authentication - [Making HTTP Requests](https://docs.ntex.rs/13-client.html): the `ntex::client` HTTP/1.1 and HTTP/2 client, connection pooling, `ClientConfig` and timeouts, TLS connectors, request bodies, reading responses, and client middleware - [HTTP/2 with ntex-h2](https://docs.ntex.rs/14-h2.html): standalone HTTP/2 client and server, `ServiceConfig`, pooled and single-connection clients, streams and resets, `Capacity` and flow control, publish and control services - [MQTT with ntex-mqtt](https://docs.ntex.rs/15-mqtt.html): MQTT 3.1.1 and 5 clients and servers, `MqttServiceConfig`, publishing and acks, streaming payloads, flow control, subscriptions, routing, `MqttSink`, protocol and control services - [Timers and Time Utilities](https://docs.ntex.rs/12-time.html): timer wheel design and precision, `Millis` and `Seconds`, `sleep`, `deadline`, `timeout`, `interval`, and callback timers ## API - [ntex](https://docs.ntex.rs/api/ntex/index.html) - [ntex_service](https://docs.ntex.rs/api/ntex_service/index.html): `Service`, `Middleware`, `Pipeline`, `forward!`, `fn_layer` - [ntex::web](https://docs.ntex.rs/api/ntex/web/index.html): `App`, `HttpServer`, `middleware::from_fn` - [ntex_server::net](https://docs.ntex.rs/api/ntex_server/net/index.html): `build`, `build_with_config`, `ServerBuilder`, `test_server` - [ntex::tls](https://docs.ntex.rs/api/ntex/tls/index.html): acceptors, connectors, `TlsConfig`, and portable `x509` identities and trust ## Optional - [Full guide](https://docs.ntex.rs/llms-full.txt): every guide chapter in one file - [Examples](https://github.com/ntex-rs/examples) - [Source](https://github.com/ntex-rs/ntex)