Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
noema-actix-webapi
Actix-web backend runtime on Noema: modules, Postgres, unit of work, Swagger, WebSocket → subscribe!.
use ;
use MapSource;
use *;
async
start / Application::start(modules, source, config) reads required DATABASE_URL from the source, installs ApplicationConfig (singleton), Postgres, migrators, infra, logger. It does not run Actix. You own HttpServer. Swagger UI and {scope}/docs/openapi.json are always mounted but gated by Presentation::authorize_docs (default deny). .health() / .ready() are opt-in. CORS is a wrap on your App, not inside configure.
Mount WebSockets on the module scope (as many routes / transports as you want):
ChatWs is your type: EventDispatcherContext (use noema_actix_webapi::actix::dispatch_context()), subscribe!(ChatWs, …), dependency!(singleton, ChatWs), and WsConnection.
Auth runs once on handshake. Event handlers do not re-check the token. SessionContext<T> / RequestContext<T> bind one T per task (the wrap / connect::<T>()). Mixing extras on the same task is unsupported.
// In an EventListener — reply to this socket; read Ctx only if you need identity:
async
Incoming text is { "name", "data" } → dispatch. Outgoing hub messages are { name: E::WIRE_NAME, data }. Handler Err, unknown name, and invalid JSON go to WsConnection::on_dispatch_error (default: { "name": "error", "data": ErrorBody } on this socket). Override the method to silence or remap.
Rooms on the hub are local last-mile (which sockets on this process get the frame). Membership for the product lives in your store. Multi-pod: broadcast_event sends locally then, if you registered a hook, publishes a WsFanoutMessage (origin, room, body). Other pods deserialize, skip is_local(), then broadcast_raw (does not publish again).
app.on_ws_publish;
// subscriber BackgroundTask:
let msg: WsFanoutMessage = from_str?;
if msg.is_local
.broadcast_raw;
process_origin() is a UUID v7 generated once in start (so a bus subscriber can skip this process). Not configurable.
Layout
| Piece | Role |
|---|---|
Application::start / start |
DATABASE_URL from source + ApplicationConfig → pool → migrators → infra → logger |
Application::configure |
scopes + Swagger (+ optional /health /ready) |
Module / Presentation / Infrastructure |
scopes, OpenAPI, migrator() |
PgPool |
one process pool from DATABASE_URL (default max 32, acquire 5s). A read replica / snapshot is an app type + its own sqlx pool, not a second PgPool |
UnitOfWork / with_transaction! |
opaque Tx; infra uses db::postgres_tx |
/swagger-ui |
always mounted; gated by authorize_docs (default deny); one tab per module |
ws::connect::<T>() |
per-route WS; on_connect then { name, data } → dispatch; errors → on_dispatch_error |
SessionHub |
local rooms + broadcast_event / broadcast_raw; opt-in on_ws_publish |
Session::<T>::get() |
connect-time Ctx during dispatch |
SessionContext<T> |
injectable port; resolve reads the bound session |
RequestScope<T> |
opt-in wrap snapshot; id + idempotency + extra |
RequestContext<T> |
injectable port; resolve reads the bound scope |
Logger |
resolve::<dyn Logger + Send + Sync>() (default TracingLogger) — do not also dependency_as! Logger in the app |
Hasher |
resolve::<dyn Hasher + Send + Sync>() (default Argon2Hasher) — same: one binding in this crate; another algorithm is your type, not a second Hasher |
Clock |
resolve::<dyn Clock + Send + Sync>() (default SystemClock) |
HttpClient |
resolve::<dyn HttpClient + Send + Sync>() — one pooled reqwest::Client |
cors() / cors_from |
opt-in wrap from CorsConfig; not mounted in configure |
PageRequest / PageResult<T> |
pagination query + JSON (ToSchema) |
HttpResult / HttpError |
presentation routes: send()?; JSON { code, message, details? } |
BackgroundTask / spawn_background_tasks |
process-lifetime loops (init then run); pass instances, no many-batch |
send |
mediator (prelude); route validates, then send(input) |
Application prelude: noema_actix_webapi::prelude. Do not import db::postgres_tx there.
Unit of work
DATABASE_URL is required on the ConfigSource passed to start (EnvSource, MapSource, …). Pool size is ApplicationConfig.database (max_connections default 32, acquire_timeout_ms default 5000). Size the pool under Postgres max_connections and against Actix workers × in-flight queries. A full pool waits up to the acquire timeout then surfaces PoolTimedOut (503). UnitOfWork uses this pool only. For a read replica, open another sqlx::PgPool in the app and inject it as your own type. resolve::<ApplicationConfig>() after start.
with_transaction!;
let uow = ;
uow.transaction.await?;
Logger and hasher
LogConfig (on ApplicationConfig): level (default info), optional file, stdout (default true), json (default false).
This crate already dependency_as! Logger → TracingLogger and Hasher → Argon2Hasher. A second dependency_as!(singleton, Hasher: …) in the app does not compile. Use resolve::<dyn Hasher + Send + Sync>() for passwords, or your own port if you need a different algorithm.
let log = ;
log.info;
let hasher = ;
let hashed = hasher.hash;
assert!;
HTTP handlers stay thin: validate input, then send(CreateUser { .. }).await.
Errors
HttpResult / HttpError are presentation (module routes), not domain. Handlers keep returning their own errors; send boxes them. Routes:
async
Map a module domain error in presentation: impl From<UserError> for HttpError using MappedError::not_found / conflict / … .
map_error understands ValidationError (400 validation.failed) and sqlx::Error: RowNotFound → 404 not_found (typical of fetch_one with zero rows; fetch_optional is Ok(None) and the app maps that itself); pool timeout/closed/crashed → 503 infrastructure.unavailable; other sqlx → 502 infrastructure.database. sqlx details.reason (not 404) is included only when ApplicationConfig.http.env is Environment::Development (default Production). /ready ping failures follow the same rule. Anything else is 500 internal.
WebSocket: the connect loop calls WsConnection::on_dispatch_error. The default uses error_ws_envelope → { "name": "error", "data": ErrorBody } on the socket that sent the frame. Invalid JSON uses code bad_request; a handler MappedError keeps its code (e.g. bad_request). Override on_dispatch_error to drop or customize the reply.
Pagination and validation
PageRequest query params page (default 1) and page_size (default 20, max 100). PageResult { items, page, page_size, total } — both have utoipa schemas.
page.validate?;
require_non_empty?;
require_email?;
Clock
let clock = ;
let _now = clock.now;
HTTP client
One process-wide reqwest::Client (connection pool per host + TLS reuse). Do not build a new client per call. Do not retry. If too many sends are in flight, the extra call fails immediately (HttpClientError::is_busy).
HttpClientConfig (on ApplicationConfig): timeout_ms (30000), connect_timeout_ms (10000), pool_max_idle_per_host (32), pool_idle_timeout_secs (optional), user_agent (noema-actix-webapi), max_in_flight (64; 0 = unlimited).
let http = ;
let resp = http.send.await?;
let created = http
.send
.await?;
Unit tests inject a fake HttpClient (no reqwest).
CORS
Opt-in wrap, same as request_context. Empty origins (Vec, the default) allows none. "*" in the list allows any origin (credentials are skipped). With *, Actix echoes the request Origin header (it is not the literal *).
CorsConfig: origins, methods, headers as Vec<String>; credentials (default false); max_age (3600).
Last .wrap() is outermost. Put cors() after request_context so a 401 from from_request still gets Access-Control-Allow-Origin. connect() also stamps the same policy on the handshake (101 and WsError) because actix-cors does not reliably cover WebSocket upgrades.
new
.wrap
.wrap
.configure
Swagger
Mounted by configure. Default authorize_docs is deny (401). The UI requires every module to allow; {scope}/docs/openapi.json uses that module only.
Background tasks
Pass the loops after start (does not run HttpServer). Each task inits then runs; if init fails, run is skipped.
let app = start.await?;
app.spawn_background_tasks;
Request context
Opt-in wrap. The crate fills id and Idempotency-Key; T is yours (() if you only need the frame). The wrap and connect::<T>() each bind one extra type per task.
new
.wrap
.configure
// production handler field:
// ctx: Arc<dyn RequestContext<Principal> + Send + Sync>
let ctx = ;
ctx.id;
ctx.idempotency_key;
ctx.extra;
Unit tests inject a double (no Actix):
let h = CreateUserHandler ;
Same for WebSocket: Arc<dyn SessionContext<ChatWs> + Send + Sync> in production via resolve, Session::new(id, ctx) in tests.