Skip to content

Configuration

Importing HttpModule::for_root(...) in AppModule.imports attaches the HTTP transport at boot. Pass None to read every option from the environment, or pin HttpConfig in code:

apps/hello/src/module.rs
use nest_rs_core::module;
use nest_rs_http::{HttpConfig, HttpModule};
use features::hello::HelloHttpModule;
#[module(
imports = [
HttpModule::for_root(HttpConfig { port: 3000, ..Default::default() }),
HelloHttpModule,
],
)]
pub struct HelloModule;
apps/api/src/module.rs
use nest_rs_http::{HttpConfig, HttpModule};
#[module(imports = [
HttpModule::for_root(HttpConfig { port: 3002, ..Default::default() }),
])]
pub struct ApiModule;

Default values: host 0.0.0.0, port 3000, no TLS (plain HTTP), no CORS, no framework Server header, a 2 MiB request-body cap, a 30-second request timeout, fail-secure boot on, and the default security headers on.

Every field of HttpConfig is settable both by env var and in the pinned struct — the same dual-path rule every config in the framework follows. Real env always wins over the .env cascade.

FieldEnv variableDefaultPinned form
hostNESTRS_HTTP__HOST0.0.0.0HttpConfig { host: "127.0.0.1".into(), ..Default::default() }
portNESTRS_HTTP__PORT3000HttpConfig { port: 3002, ..Default::default() }
tls.certNESTRS_HTTP__TLS_CERT (inline PEM) or NESTRS_HTTP__TLS_CERT_FILE (path)unset ⇒ plain HTTPHttpConfig { tls: Some(TlsConfig::new(cert, key)), ..Default::default() }
tls.keyNESTRS_HTTP__TLS_KEY or NESTRS_HTTP__TLS_KEY_FILEunset ⇒ plain HTTP(set together with tls.cert)
cors.originsNESTRS_HTTP__CORS_ORIGINS (comma list)unset ⇒ CORS offCorsConfig { origins: vec!["https://app.example.com".into()], ..Default::default() }
cors.methodsNESTRS_HTTP__CORS_METHODSemptymethods: vec!["GET".into(), "POST".into()]
cors.headersNESTRS_HTTP__CORS_HEADERSemptyheaders: vec!["Content-Type".into()]
cors.exposed_headersNESTRS_HTTP__CORS_EXPOSEDemptyexposed_headers: vec!["X-Total-Count".into()]
cors.credentialsNESTRS_HTTP__CORS_CREDENTIALS (true/false)falsecredentials: true
cors.max_ageNESTRS_HTTP__CORS_MAX_AGE (seconds)unsetmax_age: Some(Duration::from_secs(3600))
server_headerNESTRS_HTTP__SERVER_HEADER (true/false)falseHttpConfig { server_header: true, ..Default::default() }
global_prefixNESTRS_HTTP__GLOBAL_PREFIXunset ⇒ no prefixHttpConfig::default().with_global_prefix("/api")
max_body_bytesNESTRS_HTTP__MAX_BODY_BYTES (bytes)2 MiBHttpConfig::default().with_max_body_bytes(4 * 1024 * 1024)
request_timeout_secsNESTRS_HTTP__REQUEST_TIMEOUT_SECS (seconds)30HttpConfig { request_timeout_secs: Some(15), ..Default::default() }
fail_secure_strictNESTRS_HTTP__FAIL_SECURE_STRICT (true/false)trueHttpConfig { fail_secure_strict: false, ..Default::default() }
security_headersNESTRS_HTTP__SECURITY_HEADERS (master), __FRAME_OPTIONS, __HSTS, __CONTENT_TYPE_OPTIONSon (safe values)HttpConfig { security_headers: SecurityHeadersConfig { enabled: false, ..Default::default() }, ..Default::default() }
compressionNESTRS_HTTP__COMPRESSION (true/false)falseHttpConfig { compression: true, ..Default::default() }

See Compression for what the flag negotiates and when to leave it off.

Setting both NESTRS_HTTP__TLS_CERT[_FILE] and NESTRS_HTTP__TLS_KEY[_FILE] makes the transport serve over rustls (through poem’s listener) instead of plain HTTP. Setting only one of the pair fails the boot — a half-configured TLS is a deployment mistake, not a silent fall back to plaintext.

Terminal window
# Inline (suits k8s secrets, systemd EnvironmentFile, …)
NESTRS_HTTP__TLS_CERT="$(cat fullchain.pem)" \
NESTRS_HTTP__TLS_KEY="$(cat privkey.pem)" \
nestrs run dev api
# Or by path (the transport reads the file at boot)
NESTRS_HTTP__TLS_CERT_FILE=/etc/letsencrypt/.../fullchain.pem \
NESTRS_HTTP__TLS_KEY_FILE=/etc/letsencrypt/.../privkey.pem \
nestrs run dev api

Pinning TLS material in code uses TlsConfig::new — rarely useful outside tests (production deploys carry secrets in the environment):

use nest_rs_http::{HttpConfig, HttpModule, TlsConfig};
let cert = std::fs::read("fullchain.pem")?;
let key = std::fs::read("privkey.pem")?;
#[module(imports = [
HttpModule::for_root(HttpConfig {
port: 3002,
tls: Some(TlsConfig::new(cert, key)),
..Default::default()
}),
])]
pub struct ApiModule;

CORS uses poem’s Cors middleware under the hood. The transport installs it outermost, so a preflight (OPTIONS) is answered before any guard or interceptor runs.

CORS activates only when cors.origins is non-empty — the default is no CORS layer. Set the origins (and any other knob you need) via either path:

.env.production
NESTRS_HTTP__CORS_ORIGINS=https://app.example.com,https://admin.example.com
NESTRS_HTTP__CORS_METHODS=GET,POST,PUT,DELETE
NESTRS_HTTP__CORS_HEADERS=Content-Type,Authorization
NESTRS_HTTP__CORS_CREDENTIALS=true
NESTRS_HTTP__CORS_MAX_AGE=3600
apps/api/src/module.rs
use std::time::Duration;
use nest_rs_http::{CorsConfig, HttpConfig, HttpModule};
#[module(imports = [
HttpModule::for_root(HttpConfig {
port: 3002,
cors: Some(CorsConfig {
origins: vec!["https://app.example.com".into()],
methods: vec!["GET".into(), "POST".into()],
headers: vec!["Content-Type".into(), "Authorization".into()],
credentials: true,
max_age: Some(Duration::from_secs(3600)),
..Default::default()
}),
..Default::default()
}),
])]
pub struct ApiModule;

origins: vec!["*".into()] is allowed for fully open APIs (the wildcard is passed straight through to poem).

Behind a reverse proxy that hands off a sub-path (/api/*), every controller can be mounted under one prefix without touching path = "…" on each. Like every other field it follows the dual path — set it in the environment:

.env.production
NESTRS_HTTP__GLOBAL_PREFIX=/api

or pin it in code with with_global_prefix:

apps/api/src/module.rs
use nest_rs_http::{HttpConfig, HttpModule};
#[module(imports = [
HttpModule::for_root(HttpConfig::default().with_global_prefix("/api")),
])]
pub struct ApiModule;

The prefix is normalized ("api", "/api", "/api/" all yield Some("/api"); empty / "/" collapse to no-op), then prepended to every route at mount time — #[get("/users")] ends up at GET /api/users. The boot log and the OpenAPI document reflect the prefix.

Off by default — a production-safe choice: no fingerprint of the framework or its version is exposed. Flip on for local development to see Server: nestrs/<crate version> on every response (the same shape Apache and nginx use):

.env.development
NESTRS_HTTP__SERVER_HEADER=true
HttpModule::for_root(HttpConfig {
server_header: true,
..Default::default()
})

The value is sourced from the nest-rs-http crate’s CARGO_PKG_VERSION at build time — it tracks the framework, not your app version.

RawBody (and every extractor built on it) accepts at most max_body_bytes2 MiB by default, so a runaway upload can’t exhaust memory before a handler ever sees it. Raise or lower it globally:

.env.production
NESTRS_HTTP__MAX_BODY_BYTES=4194304 # 4 MiB

A single route that needs a different cap overrides it per call with RawBody::extract_with_limit; the config value is the default for everything else.

request_timeout_secs bounds how long one request may run. A handler that exceeds it is aborted and the client gets 504 Gateway Timeout, so a slow or stuck request can’t tie up a connection indefinitely. Default 30; set None (env unset with the field pinned to None) to disable:

.env.production
NESTRS_HTTP__REQUEST_TIMEOUT_SECS=15

On by default — a freshly-scaffolded app ships safe headers without having to remember them:

  • X-Content-Type-Options: nosniff — defeats MIME sniffing.
  • X-Frame-Options: DENY — no framing (clickjacking).
  • Strict-Transport-Security: max-age=31536000; includeSubDomains — emitted only when TLS is active (HSTS over plain HTTP is meaningless and a footgun on localhost).

Every value is tunable through the dual path. Disable the whole set with the master switch, or drop one header by setting its value to an empty string:

.env
NESTRS_HTTP__SECURITY_HEADERS=false # all off
NESTRS_HTTP__FRAME_OPTIONS=SAMEORIGIN # override one value
NESTRS_HTTP__HSTS= # empty ⇒ drop just HSTS
use nest_rs_http::{HttpConfig, HttpModule, SecurityHeadersConfig};
HttpModule::for_root(HttpConfig {
security_headers: SecurityHeadersConfig {
frame_options: Some("SAMEORIGIN".into()),
..Default::default()
},
..Default::default()
});

fail_secure_strict is true by default: when global guards are registered and an endpoint the transport can’t shape (an imperative mount(...)) would bypass the guard pool, the boot fails naming the offending mount rather than silently serving it unguarded. Setting it to false downgrades that failure to a warn — a deliberate opt-out, not the default:

.env
NESTRS_HTTP__FAIL_SECURE_STRICT=false

Built by YV17labs