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 nest_rs_opentelemetry::OpenTelemetryModule;
use features::hello::HelloHttpModule;
#[module(
imports = [
OpenTelemetryModule,
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.

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() }

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:

apps/api/src/main.rs
use nest_rs_core::App;
use nest_rs_http::HttpTransport;
use api::ApiModule;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
App::builder()
.module::<ApiModule>()
.transport(HttpTransport::new().global_prefix("/api"))
.build()
.await?
.run()
.await
}

HttpTransport::global_prefix("/api") normalizes the input ("api", "/api", "/api/" all yield Some("/api"); empty / "/" collapse to no-op), then prepends it 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.