A3S Boot
Overview
A3S Boot is a progressive Rust web framework crate for building modular A3S services. It takes the architectural ideas that make Nest.js useful for growing services and expresses them in explicit, idiomatic Rust:
- explicit application modules
- importable feature modules
- typed providers resolved through
ModuleRef - controller route groups
- framework-neutral route definitions and requests
- global, controller-level, and route-level pipes, guards, interceptors, and exception filters
- protocol-neutral execution context for shared guards and observers
- typed JSON DTO helpers for controller inputs and responses
- replaceable HTTP adapters
- a single application builder
- startup and shutdown module lifecycle hooks
Rust does not have TypeScript's runtime decorator metadata model. A3S Boot
supports Nest-style Rust attribute macros through a3s-boot-macros, and those
macros expand at compile time into the same explicit module, provider, and
controller definitions used by the core API. Axum is the default adapter, not
the framework kernel. If you are coming from Nest.js, see
Nest-Style Attribute Macros for the
@Injectable and @Controller style. See ROADMAP.md for the
Nest parity development plan.
Quick Start
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional ACL-backed configuration module when the application needs typed runtime configuration:
[]
= { = "0.1", = ["config"] }
= { = "1", = ["derive"] }
Enable the optional in-memory cache module when the application needs a typed cache provider:
[]
= { = "0.1", = ["cache"] }
= { = "1", = ["derive"] }
Enable the optional authentication module when the application needs Nest-style strategy-backed guards:
[]
= { = "0.1", = ["auth"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional database module when the application needs a provider-backed database facade with replaceable backends:
[]
= { = "0.1", = ["database"] }
= { = "1", = ["derive"] }
= "1"
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional scheduler module when the application needs Nest-style scheduled jobs:
[]
= { = "0.1", = ["schedule"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional in-process queue module when the application needs provider-backed background job processing:
[]
= { = "0.1", = ["queue"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional TCP microservice transport when services need newline-delimited JSON message patterns over a network socket:
[]
= { = "0.1", = ["tcp-transport"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional Redis microservice transport when services need Nest-style request-response and event-only message patterns over Redis Pub/Sub:
[]
= { = "0.1", = ["redis-transport"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional NATS microservice transport when services need Nest-style request-response and event-only message patterns over NATS subjects:
[]
= { = "0.1", = ["nats-transport"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional MQTT microservice transport when services need Nest-style request-response and event-only message patterns over MQTT topics:
[]
= { = "0.1", = ["mqtt-transport"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional RabbitMQ microservice transport when services need Nest-style request-response and event-only message patterns over AMQP queues:
[]
= { = "0.1", = ["rabbitmq-transport"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional Kafka microservice transport when services need Nest-style request-response and event-only message patterns over Kafka topics:
[]
= { = "0.1", = ["kafka-transport"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional gRPC microservice transport when services need Nest-style request-response and event-only message patterns over a unary gRPC service:
[]
= { = "0.1", = ["grpc-transport"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional structured logger module when the application needs provider-backed logging without forcing a concrete backend:
[]
= { = "0.1", = ["logging"] }
Enable the optional outbound HTTP client module when providers need to call external HTTP services:
[]
= { = "0.1", = ["http-client"] }
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional CQRS module when an application wants Nest-style command, query, and event buses:
[]
= { = "0.1", = ["cqrs"] }
= { = "1", = ["macros", "rt-multi-thread"] }
Enable the optional gzip compression interceptor when the application should
compress eligible responses for clients that send Accept-Encoding: gzip:
[]
= { = "0.1", = ["compression"] }
Enable optional multipart file upload helpers when handlers need to parse
multipart/form-data requests:
[]
= { = "0.1", = ["file-upload"] }
Enable the optional static file module when the application should serve built frontend assets or an SPA shell:
[]
= { = "0.1", = ["static"] }
= { = "1", = ["macros", "rt-multi-thread"] }
use ;
;
;
async
Run the example:
Nest-Style Attribute Macros
The default a3s-boot features include a3s-boot-macros, so applications can
write Rust attributes that feel close to Nest.js decorators:
| Nest.js decorator | A3S Boot attribute macro |
|---|---|
@Injectable() |
#[injectable] on a service struct |
@Controller("cats") |
#[controller("/cats")] on an inherent impl block |
@Controller({ host: ":account.example.com" }) |
#[host("{account}.example.com")] below #[controller] |
@Get(":id") |
#[get("/{id}")] on an async method |
@Post() |
#[post("/", status = 201)] on an async method |
@All("catch") |
#[all("/catch")] on an async method that handles every standard HTTP method |
@Param("id") |
#[param("id")] on a method argument |
@Param("id", ParsePipe) |
#[param("id", pipe = parse_cat_id)] on a method argument |
@HostParam("account") |
#[host_param("account")] on a method argument |
@Query() / @Query("page") |
#[query] for a DTO or #[query("page")] for one value |
@Query("page", ParsePipe) |
#[query("page", pipe = parse_page)] on a method argument |
@Body() |
#[body] on a JSON body DTO argument |
@Headers("x-request-id") |
#[header("x-request-id")] on a method argument |
@Ip() |
#[ip] on a method argument |
@Req() |
#[request] on a BootRequest argument |
createParamDecorator(...) |
#[extract(current_user)] with a RequestExtractor<T> or function |
@Sse("events") |
#[sse("/events")] on an async method returning an SSE event stream |
@WebSocketGateway() |
#[websocket_gateway("/ws")] on an inherent impl block |
@SubscribeMessage("cat.find") |
#[subscribe_message("cat.find")] on an async gateway method |
| Microservice controller | #[message_controller] on an inherent impl block |
@MessagePattern("cat.find") |
#[message_pattern("cat.find")] on an async message method |
@EventPattern("cat.created") |
#[event_pattern("cat.created")] on an async event method |
@Payload() |
A typed message method argument deserialized from TransportMessage::data |
@OnEvent("cat.created") |
#[on_event("cat.created")] inside #[event_listener] |
@UseGuards(AuthGuard) |
#[use_guard(AuthGuard)] on a controller impl or route method |
@UseInterceptors(TraceInterceptor) |
#[use_interceptor(TraceInterceptor)] on a controller impl or route method |
@UseFilters(HttpErrorFilter) |
#[use_filter(HttpErrorFilter)] on a controller impl or route method |
@Catch(BadRequestException) |
catch_errors([BootErrorKind::BadRequest], BadRequestFilter) inside #[use_filter(...)] or with_catch_filter(...) |
@UsePipes(ParsePipe) |
#[use_pipe(ParsePipe)] on a controller impl or route method |
@UsePipes(new ValidationPipe()) |
#[validate] on a controller impl or route method for DTO validation |
@SetMetadata("roles", ["admin"]) |
#[metadata("roles", ["admin"])] below #[controller] or on a route method |
@Version("1") |
#[version("1")] below #[controller] or on a route method |
@Version(["1", "2"]) |
#[versions("1", "2")] below #[controller] or on a route method |
VERSION_NEUTRAL |
#[version_neutral] below #[controller] or on a route method |
@SerializeOptions(...) |
#[serialize(include = ["id"], exclude = ["password"], skip_null)] below #[controller] or on a route method |
@Cron("0 0 0 * * * *") |
#[cron("cats.prune", "0 0 0 * * * *")] inside #[schedule] |
@Interval("cats.refresh", 60000) |
#[interval("cats.refresh", 60000)] inside #[schedule] |
@Timeout("cats.warmup", 5000) |
#[timeout("cats.warmup", 5000)] inside #[schedule] |
@HttpCode(202) |
#[http_code(202)] on a JSON route method |
@Header("cache-control", "max-age=60") |
#[header("cache-control", "max-age=60")] on a route method |
@Redirect("/new", 301) |
#[redirect("/new", status = 301)] on a route method |
@ApiTags("cats") |
#[tag("cats")] below #[controller] |
@ApiOperation(...) |
#[operation(summary = "...", operation_id = "...")] on a route method |
@ApiResponse(...) |
#[response(status = 200, description = "...", schema = CatDto)] |
@ApiBearerAuth() |
#[bearer_auth] on a route method |
| Constructor injection | #[injectable] fields such as cats: Arc<CatsService> plus CatsController::provider() |
@Inject("TOKEN") |
#[inject("token")] on an Arc<T> or Option<Arc<T>> field |
@Optional() |
Option<Arc<T>> on an injectable field |
@Module({ providers, controllers, imports }) |
impl Module with providers(), controllers(), and imports() |
NestFactory.create(AppModule) |
BootFactory::create(AppModule)? |
app.listen(3000) |
app.listen_with(&AxumAdapter::new(), addr).await |
app.close() |
app.close().await |
NestFactory.createApplicationContext(...) |
BootFactory::create_application_context(...) |
NestFactory.createMicroservice(...) |
BootFactory::create_microservice(...) |
These are Rust procedural macros, not TypeScript runtime decorators. They
generate ordinary ProviderDefinition, ControllerDefinition, and
MessagePatternDefinition values at
compile time. The explicit API remains available and is what the macros expand
into:
use Arc;
use ;
use ;
;
;
async
#[injectable] adds auto-wired provider helpers such as provider() and
request_scoped_provider(), plus explicit value helpers such as
into_provider() and from_arc_provider(...). It auto-wires fields shaped as
Arc<T> or Option<Arc<T>>; add #[inject("token")] on a field to resolve a
named provider. #[controller("/cats")] adds a
controller(self: Arc<Self>) method that collects route attributes from the
impl block. GET, POST, PUT, PATCH, and DELETE route attributes default to JSON.
Use extractor attributes on method arguments for Nest-style request binding:
#[param("id")], #[params], #[query], #[query("name")], #[body],
#[header("name")], #[headers], #[host_param("account")], #[ip], and
#[request]. Single-value extractors parse into the argument type with
FromStr, so #[param("id")] id: u64 and #[query("active")] active: bool
work without a separate parse pipe. For custom Nest-style parameter pipes, add
pipe = <expr> to #[param], #[query("name")], #[header],
#[host_param], or #[ip]; the pipe receives the raw String and returns
Result<T>:
;
async
Add raw only when the method should return
Result<BootResponse> directly, for example #[get("/health", raw)]. The
explicit *_json route attributes remain available as compatibility aliases,
but typical code should use #[get] and #[post] directly.
#[sse("/events")] registers a GET endpoint that returns a
text/event-stream response and accepts any stream whose items are
Result<SseEvent>.
Application Factory
BootFactory is the NestFactory-style entrypoint for managed startup and
shutdown. create(...) returns an application handle with init(),
listen_with(...), close(), and provider lookup helpers. Use
create_application_context(...) for provider-only workers, and
create_microservice(...) for standalone message transports. When a module
registers async provider factories, use the async variants:
create_async(...), create_application_context_async(...), or
create_microservice_async(...).
use ;
async
Custom parameter decorators use #[extract(...)], where the expression
implements RequestExtractor<T> or is a function that takes &BootRequest and
returns Result<T>:
use ;
;
Manual handlers can use the same parsing rules through
BootRequest::param_as::<T>(), query_value_as::<T>(),
query_values_as::<T>(), header_as::<T>(), host_param_as::<T>(), and
ip_as::<T>().
Host-scoped controllers mirror Nest's host-based controller option. Put
#[host("{account}.example.com")] below #[controller] to constrain every
route in that controller, or put #[host("api.example.com")] on a route method
to override the controller default:
use ;
;
Host parameters accept both Boot's {account}.example.com style and Nest's
:account.example.com style in explicit route definitions. #[ip] reads the
standard forwarding headers (Forwarded, X-Forwarded-For, then X-Real-Ip)
as an adapter-neutral client IP hint; when the argument is not Option<T>,
missing or invalid values map to BootError::BadRequest.
Validation Pipeline
DTO validation is explicit. Implement Validate for request DTOs, then enable
validation globally, at controller scope, or at route scope. Invalid DTOs map to
BootError::BadRequest, which adapters expose as HTTP 400. The
post_validated_json / put_validated_json / patch_validated_json helpers are
route-level shortcuts; global and controller-level validation run validators
registered on each route.
use ;
use ;
let route = post_validated_json?;
let controller = new?
.with_validation
.route?;
let app = builder
.use_global_validation
.route
.build?;
For Nest-style controllers, put #[validate] below #[controller]. It adds
validators for #[body], #[query], and #[params] DTO arguments. Use
#[skip_validation] on a route method when a controller-level validation policy
should not apply to that method.
Manual handlers can also call BootRequest::validated_json::<T>(),
validated_query::<T>(), or validated_params::<T>(). Raw handlers are not
validated unless they register validators explicitly, for example with
RouteDefinition::with_body_validation::<T>().
Server-Sent Events
SSE routes mirror Nest.js @Sse() endpoints: handlers return a stream of
SseEvent values, and Boot sends them as text/event-stream chunks through the
selected adapter. SseEvent::stream(...) is a small helper for finite streams;
long-running handlers can return any Stream<Item = Result<SseEvent>>.
use ;
SSE routes require clients to accept text/event-stream; missing Accept,
*/*, text/*, and text/event-stream are accepted, while requests that only
accept unrelated media types return BootError::NotAcceptable.
API Versioning
Boot supports Nest-style API versioning without coupling route matching to a specific HTTP adapter. Enable one version extraction strategy on the application, then attach versions to individual routes or a whole controller.
use ;
let app = builder
.enable_api_versioning
.route
.route
.build?;
With URI versioning, /v1/cats/milo matches the route path /cats/{id};
handlers receive decoded params from the unversioned route shape. A
version-neutral route such as /health matches any requested version.
Controller-level versions are inherited by routes that do not declare their own version:
use ;
The macro form mirrors Nest's @Version() decorator. Put #[version("1")],
#[versions("1", "2")], or #[version_neutral] below #[controller] or on a
route method:
use ;
;
Header and media type strategies use the same route metadata:
use ;
let header_versioned = builder
.enable_api_versioning
.route
.route
.build?;
let media_type_versioned = builder
.enable_api_versioning
.route
.build?;
For header versioning, send x-api-version: 2. For media type versioning, send
an Accept value such as application/json; v=2. Routes without explicit
version metadata match unversioned requests, and also match the configured
default version when one is set with .with_default_version("1").
OpenAPI Metadata
Boot can generate an OpenAPI 3 document from resolved routes. Route metadata is
adapter-neutral and can be added with builder methods or Nest-style metadata
macros. serve_openapi(...) mounts a generated JSON document without including
that document route in its own output. Schema components can be registered
manually, or generated from schemars::JsonSchema when the openapi-schemas
feature is enabled.
use ;
use Serialize;
let route = get_json?
.with_tag
.with_operation_id
.with_summary
.with_query_parameter
.with_json_response
.with_response
.with_schema_component;
let app = builder
.route
.serve_openapi
.build?;
let document = app.openapi;
let json = to_value?;
Path parameters are inferred from {name} route segments and documented as
required string parameters unless the route supplies a more specific path
parameter schema. ControllerDefinition::with_tag("cats") applies a tag to all
routes registered after it, similar to Nest Swagger's @ApiTags. In macro
controllers, #[param("id")], #[query("name")], #[header("name")], and
#[body] also add matching OpenAPI parameter or request-body metadata.
With openapi-schemas, routes can collect component schemas directly from
types that derive schemars::JsonSchema:
let route = get_json?
.with_json_response
.?;
WebSocket Gateways
WebSocket gateways mirror Nest's @WebSocketGateway() and
@SubscribeMessage() style while keeping the runtime adapter-neutral. Messages
are JSON objects with an event string and optional data value. The Axum
adapter registers gateway paths as WebSocket upgrade routes behind the axum
feature.
use Arc;
use ;
use Serialize;
;
;
The explicit API is available for tests, dynamic modules, and adapters:
use ;
use json;
async
Gateway-specific WebSocketPipe, WebSocketGuard, and WebSocketInterceptor
hooks run in deterministic order: guards, interceptor before, pipes, handler,
then interceptor after in reverse order. They are separate from HTTP
middleware because WebSocket message dispatch is event-based rather than
request/response-based, but they follow the same Nest-style pipeline order.
Microservice Transports
Microservice message patterns mirror Nest's @MessagePattern() and
@EventPattern() style. The core is adapter-neutral: messages are JSON-like
TransportMessage values with a pattern and data, and external brokers can
implement MessageTransport. InProcessTransport is included for tests,
workers, and single-process dispatch. Enable the tcp-transport feature to use
TcpTransport for newline-delimited JSON messages over TCP, or
redis-transport to use Redis Pub/Sub channels, or nats-transport to use
NATS subjects, mqtt-transport to use MQTT topics, or rabbitmq-transport to
use RabbitMQ queues, kafka-transport to use Kafka topics, or
grpc-transport to use Boot's unary gRPC message service.
use Arc;
use ;
use ;
;
;
#[message_pattern("cat.find")] defaults to JSON responses, so returning
Result<CatDto> becomes a TransportReply automatically. Use
#[message_pattern("cat.find", raw)] when a handler should return
TransportReply directly. Typed method arguments are deserialized from
TransportMessage::data; use TransportMessage as the argument type when the
handler needs raw access to the pattern and payload.
The explicit API is available for dynamic registration and validation:
use ;
use ;
async
Transport-specific TransportPipe, TransportGuard, and
TransportInterceptor hooks run in deterministic order: guards, interceptor
before, pipes, validation, handler, then interceptor after in reverse order.
With tcp-transport, the same message patterns can be served over a network
socket:
use SocketAddr;
use ;
async
async
The wire format is one UTF-8 JSON frame per line. Clients send a
TransportMessage such as {"pattern":"cat.find","data":{"id":"1"}}; servers
reply with a reply, no_reply, or error envelope. Handler errors are mapped
back into the closest BootError variant on the client.
With redis-transport, request-response messages go through a configured
request channel and receive replies on per-request reply channels. Event
messages are published to a configured event channel:
use Duration;
use ;
async
async
With nats-transport, request-response messages use NATS request/reply on a
configured subject. Event messages are published to a separate subject. Use a
queue group when multiple service instances should share work:
use Duration;
use ;
async
async
With mqtt-transport, request-response messages are published to a configured
request topic and receive replies on per-request reply topics. Event messages
are published to a separate event topic:
use Duration;
use ;
async
async
With rabbitmq-transport, request-response messages are published to a
configured request queue and receive replies on exclusive per-request reply
queues. Event messages are published to a separate event queue:
use Duration;
use ;
async
async
With kafka-transport, request-response messages are published to a configured
request topic and receive replies on per-request reply topics. Event messages
are published to a separate event topic:
use Duration;
use ;
async
async
With grpc-transport, request-response and event-only messages are served by a
fixed unary gRPC service. Payloads still use the same adapter-neutral
TransportMessage shape, so the same #[message_pattern] and
#[event_pattern] handlers work without broker-specific code:
use SocketAddr;
use Duration;
use ;
async
async
Application Events
Enable the events feature to use an in-process EventEmitter provider,
similar to Nest's event emitter module. EventModule can register exact
listeners such as cat.created, prefix listeners such as cat.*, or a global
* listener. Events are dispatched asynchronously and payloads are serialized
through serde_json, so handlers can decode typed DTOs with data_as::<T>().
use Arc;
use ;
use ;
;
async
#[on_event] handlers may accept no arguments, one typed payload decoded from
the event JSON, an EventEnvelope, an EventContext, or one event argument
plus EventContext. Use EventModule::listener(...) for inline listeners that
do not need an impl-level macro.
EventModule::global() exports the emitter across module boundaries, and
EventModule::named(...) can register a named emitter provider when a service
needs separate event channels.
CQRS
Enable the cqrs feature to use CqrsModule, CommandBus, QueryBus, and
EventBus, similar to Nest's CQRS recipe. Commands and queries have exactly one
handler. Events can have multiple handlers and are published in registration
order. Handlers receive a CqrsContext, so they can resolve providers from the
same module scope.
use ;
;
async
CqrsModule can also import other modules and register local providers used by
handlers. CqrsModule::global() exports the three buses to every module after
registration. The buses can be used directly in tests with
CommandBus::register(...), QueryBus::register(...), and
EventBus::register(...).
Health Checks
Enable the health feature to expose Terminus-style health checks through a
provider-backed HealthCheckService. HealthModule::new(...) registers the
service, registers async indicators, and contributes a JSON GET /health route
by default. The route returns HTTP 200 when every indicator is up and HTTP 503
when any indicator is down or returns an error.
use ;
Use without_route() when the service should only be consumed by another
controller or host, with_route("/ready") for a custom endpoint, and
named(...) or global() when multiple modules need distinct health services
or shared readiness checks.
Providers
Providers can be registered as owned singletons, factories, shared Arc<T>
values, factories that return Arc<T>, request-scoped providers, transient
providers, or aliases to existing providers. Provider tokens are unique inside a
module scope; different modules can declare the same token without colliding.
Importing modules can only see providers that imported modules explicitly
export.
Singleton providers are the default and are built once per module. Transient
providers are built for every resolution. Request-scoped providers are built
once per in-process request scope and are cached for that request, including
dependencies resolved inside another request-scoped provider factory. Outside a
request scope, request-scoped providers behave like a fresh resolution. Provider
aliases mirror Nest's useExisting: the alias token delegates to the target
token and preserves the target provider's scope.
When an application module builds, Boot registers the module's provider tokens before it initializes singleton factories. Singleton factories can therefore depend on other providers declared later in the same module. During async application builds, async singleton factories are seeded before sync singleton factories are resolved, so sync singletons can depend on async-built singletons without declaration-order coupling.
During singleton, transient, and request-scoped provider resolution, Boot tracks
the active provider chain and reports circular dependencies with the full token
path, for example
cyclic provider dependency detected: cats -> repository -> cats.
use Arc;
use ;
;
let providers = vec!;
ModuleRef::get(...) reads from the current provider graph. Use
resolve(...), resolve_named(...), or the optional variants when you want a
fresh Nest-style resolution context: singleton providers keep their cached
instance, transient providers are rebuilt, and request-scoped dependencies share
one temporary context for that resolution. Use create::<T>() or
create_arc::<T>() to instantiate a FromModuleRef type without registering or
caching that type as a provider.
Async provider factories mirror Nest's async providers. They are awaited while
the application graph is built, before controllers and routes resolve their
dependencies. Use build_async() or a BootFactory::*_async(...) method; the
sync build() path rejects async providers with a clear error:
use Arc;
use ;
;
# async
Async provider factories are singleton-only because provider lookup is synchronous after the graph has been built. Use request-scoped or transient providers for cheap per-request/per-resolution values, and let those providers depend on async-built singletons.
Request-scoped providers are available from handlers through the request's module context:
;
Use *_scoped route helpers when the handler itself should be rebuilt for each
request scope, similar to request-scoped Nest controllers:
Singleton providers can opt into Nest-style lifecycle hooks:
use ;
;
let provider = singleton
.
.;
Lifecycle hooks require singleton scope. Request-scoped and transient providers are created for request or resolution contexts, so they do not participate in application startup or shutdown hooks.
Testing Modules
TestingModule mirrors Nest's test-module workflow: assemble a module graph,
override providers before controllers are built, compile it, resolve providers
from the compiled graph, override route pipeline components, and call the app
in process without binding a socket. Use compile_async() when the test module
contains async provider factories.
use Arc;
use ;
;
# async
Pipeline overrides use the original component type as the first generic argument and the replacement value as the method argument:
let module = builder
.import
.
.
.
.
.compile?;
Discovery And Reflector
DiscoveryService creates a read-only snapshot of a built application. It can
inspect modules, local provider tokens, resolved HTTP routes, WebSocket
gateways, and microservice message patterns. Reflector provides convenient
route metadata lookups over the same snapshot, similar to Nest's reflector
pattern.
use ;
use json;
;
;
let app = builder.import.build?;
let discovery = from_app?;
let reflector = discovery.reflector;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
Route metadata is also copied into ExecutionContext, so guards and
interceptors can enforce policy without coupling to a concrete router:
use ;
use json;
let route = get?
.with_metadata_value
.with_guard;
ExecutionContext is protocol-neutral. HTTP guards receive it directly, and
WebSocket gateways or microservice message patterns can reuse the same guard
through with_execution_guard(...) or use_global_execution_guard(...). Use
ExecutionInterceptor when the hook only needs before/after observation and
should work across HTTP, WebSocket, and transport handlers:
use ;
;
let guard = ProtocolGuard;
let route = get?
.with_guard;
let gateway = new?
.with_execution_guard
.subscribe?;
let pattern = request?
.with_execution_guard;
The macro form mirrors Nest's @SetMetadata() usage:
# use Result;
;
Module Encapsulation
Modules are provider visibility boundaries. A module can use its own providers
and the exported providers of its imports. Providers declared by an imported
module stay private unless that module returns their ProviderToken from
exports(). Import cycles are rejected with the full module chain, for example
cyclic module import detected: cats -> database -> cats.
use ;
use Arc;
;
;
let app = builder.import.build?;
let repository = app.?;
A module can re-export an imported provider by listing the imported token in its
own exports(). Global modules expose their exported providers to other module
scopes after registration:
;
Use DynamicModule when providers are assembled from runtime configuration:
use DynamicModule;
BootApplication::get(...) resolves from root module scopes and global exports.
Private providers from imported feature modules are not exposed unless they are
exported into a root-visible scope.
Lazy Module Loading
LazyModuleLoader is registered as an application-wide provider, similar to
Nest's lazy module loader. Use app.lazy_module_loader() or inject
Arc<LazyModuleLoader> from a provider factory, then call load(...) when a
module's provider graph should be created on demand:
use ;
use Arc;
;
;
let app = builder.build?;
let reports = app
.lazy_module_loader?
.load?
.?;
# let _ = reports;
Loaded modules are cached by module name. If a module was already imported during application build, the lazy loader returns that existing module reference instead of building a second provider graph. Lazy modules can import other modules and can see global exports, but they are provider-only: loading a module this way does not register controllers, direct routes, gateways, middleware, message patterns, or module/provider lifecycle hooks.
Use load_async(...) or load_arc_async(...) when the lazy module includes
async singleton provider factories:
# use ;
#
# ;
#
# ;
#
# async
Configuration
Enable the config feature to load typed configuration from ACL. ConfigModule
implements Module, registers the parsed config value as a provider, and
exports it to importing modules. This keeps configuration composition on the
same provider/module path as services and repositories.
use Arc;
use ;
use Deserialize;
let config = from_validated_acl_str?;
let app = builder
.import
.build?;
let repository = app.?;
ACL values are converted through serde into your config type. Top-level attributes map to struct fields, unlabeled blocks map to nested structs, and labeled blocks map to maps keyed by the first label:
database_url = "postgres://localhost/app"
features = ["http", "sse", "transport"]
limits {
body_bytes = 4096
}
providers "openai" {
api_key = env("OPENAI_API_KEY", "test-key")
base_url = concat("https://", "api.openai.com", "/v1")
}
Use ConfigModule::from_acl_file(...) for file-backed configuration,
ConfigModule::from_acl_str(...) for embedded configuration, and
ConfigModule::from_value(...) for already-built values. Add .named("token")
to export a named provider, or .global() to make the config visible to every
module scope after registration.
Database
Enable the database feature to register DatabaseModule and inject a
provider-backed Database facade. Boot does not force a concrete ORM or SQL
driver into the core; production adapters implement DatabaseBackend, while
tests can use InMemoryDatabaseBackend to observe statements and transaction
behavior.
use Arc;
use ;
use json;
let backend = new
.with_query_result?;
let app = builder
.import
.build?;
Use DatabaseModule::from_backend(...) for a concrete driver,
from_backend_arc(...) for shared driver handles, and .named("token") when
a module needs multiple database providers. Database::transaction(...) commits
when the callback returns Ok and rolls back when it returns an error.
HTTP Client
Enable the http-client feature to use HttpModule and HttpService, Boot's
provider-backed equivalent of Nest's HttpModule and HttpService. The default
backend uses reqwest; tests can provide any HttpClientBackend.
use Arc;
use Duration;
use ;
use Deserialize;
;
let app = builder.import.build?;
Use .named("token") when a module needs more than one HTTP client. Use
HttpModule::async_options(...) with build_async() when the options depend on
runtime providers or asynchronous setup. HttpService exposes typed helpers
such as get_json(...), post_json(...), put_json(...), and patch_json(...)
plus lower-level request(...) for explicit HttpClientRequest values.
Caching
Enable the cache feature to register a typed cache provider. CacheModule
implements Module, exports Cache, and starts with an in-memory backend for
single-process services and tests. External stores can implement CacheStore
later without changing service code.
use Arc;
use Duration;
use ;
use ;
;
let app = builder.import.build?;
let cats = app.?;
let cat = cats.find_one?;
Use Cache::set(...), get(...), remove(...), and clear(...) for direct
operations. Cache::get_or_insert_with(...) caches computed values using the
module default TTL. Add .named("token") to CacheModule when a module needs
multiple cache providers, or .global() to make one cache visible to every
module scope after registration.
Task Scheduling
Enable the schedule feature to register ScheduleModule and inject a
provider-backed Scheduler. The in-process backend supports the same core
shapes as Nest's @Timeout, @Interval, and @Cron; jobs are started during
application bootstrap and aborted during application shutdown.
use Arc;
use ;
;
let app = builder
.import
.build?;
app.bootstrap.await?;
app.shutdown.await?;
#[interval(60000)] and #[timeout(5000)] infer the job name from the method
name. Add an explicit first string argument when the job needs a stable public
name for logs, metrics, or removal. The duration argument is milliseconds, which
matches Nest's schedule decorators. A scheduled method may accept no arguments
or one ScheduleContext argument.
The lower-level API remains available for jobs that should be registered directly on the module or dynamically during application bootstrap:
use Arc;
use Duration;
use ;
;
let app = builder
.import
.build?;
app.bootstrap.await?;
app.shutdown.await?;
Use ScheduleModule::in_process("schedule").interval(...),
.timeout(...), .cron(...), or .jobs(...) for jobs that can be declared
directly on the schedule module. Use injected Scheduler registration when a
job needs to be added after bootstrap has started. Add .named("token") when a
module needs multiple scheduler providers, or .global() to make one scheduler
visible to every module scope after registration.
Queues
Enable the queue feature to register QueueModule and inject a
provider-backed Queue. The in-process backend is intended for tests,
embedded single-process workers, and adapter development; durable or distributed
backends can implement QueueBackend without changing service code.
use Arc;
use ;
use ;
;
let app = builder
.import
.build?;
let queue = app.?;
app.bootstrap.await?;
queue
.enqueue
.await?;
app.shutdown.await?;
Use QueueModule::in_process("name").processor(...) for processors that can
be declared directly on the queue module. Use injected Queue::process(...)
when a processor needs providers from the importing module. Queue::enqueue(...)
serializes payloads through serde JSON; processors can call
QueueJob::data_as::<T>() to decode typed payloads. Use Queue::jobs(),
stats(), and failures() for test assertions and local diagnostics. Add
.named("token") when a module needs multiple queue providers, or .global()
to make one queue visible to every module scope after registration.
Logging
Enable the logging feature to register LoggingModule and inject a
provider-backed structured Logger. Boot ships a NoopLogSink and an
InMemoryLogSink for tests; production adapters can implement LogSink for
their preferred backend.
use Arc;
use ;
let sink = new;
let logger = new.with_target;
let request_logger = new;
let app = builder
.use_global_interceptor
.import
.route
.build?;
app.call.await?;
let records = sink.records?;
Logger::with_target(...) and Logger::child(...) keep service, request, and
worker logs separate without changing sinks. Queue processors, scheduled jobs,
transport handlers, and WebSocket gateways can inject the same Logger
provider through ModuleRef. RequestLoggingMiddleware logs incoming requests
before the pipeline; RequestLoggingInterceptor logs route start/completion and
response status after the handler.
Middleware
Middleware runs after route matching and path parameter decoding, but before
guards, interceptor before hooks, pipes, validation, and handlers. A
middleware can mutate the BootRequest and continue, or short-circuit with a
BootResponse.
use ;
Global middleware is prepended to module middleware, then controller middleware,
then route middleware. Module middleware is returned from Module::middleware()
and applies to controllers and direct routes declared by that module. Errors
returned by middleware go through exception filters; short-circuit responses
skip the remaining request pipeline. Adapters still run their own request
validation before Boot middleware executes.
JSON DTOs
Controllers can accept typed request DTOs and return serializable response DTOs without manually parsing request bytes:
use ;
use ;
JSON body route helpers require a JSON-compatible request content type such as
application/json or application/*+json; missing or non-JSON content types map
to BootError::UnsupportedMediaType and HTTP 415. JSON response route helpers
honor Accept: requests with no Accept header, application/json,
concrete application/*+json types such as application/problem+json,
application/*, application/*+json, or */* can receive JSON; requests that
explicitly exclude JSON map to BootError::NotAcceptable and HTTP 406. Invalid
JSON and invalid UTF-8 text bodies map to BootError::BadRequest; adapters can
turn those into HTTP 400 while exception filters can override the response
shape. Manual handlers can use request.json() when they only want to parse
bytes, request.json_with_content_type() when they want the same content-type
check, and request.require_accepts_json() before returning JSON from custom
handlers. Use *_json_with_status(path, status, handler) on
ControllerDefinition or RouteDefinition when the helper should still parse
and serialize DTOs but return a non-200 status such as 201 or 202.
Empty status responses can use a dedicated helper instead of constructing an
empty byte vector, and text or JSON responses can set a status code while
preserving the right content type. In-process callers can decode response bodies
with body_text() and body_json(), or check status classes with helpers like
is_success() and is_client_error(). They can also check has_body() and
allows_body() or call validate() before handing a response to an adapter.
validate() runs status-code, Content-Length, no-body status, and response
header name/value checks in adapter order; the individual validate_status(),
validate_content_length(), validate_body_allowed(), and validate_headers()
helpers remain available for focused checks. Error responses can reuse the
framework's standard HTTP error mapping. BootErrorKind and
catch_errors(...) provide Nest-style catch filters for selected error kinds;
use with_catch_filter(...) or #[use_filter(catch_errors(...))] when a filter
should only handle errors such as BootErrorKind::BadRequest. Routes can also
attach static response headers or redirect successful handler results
declaratively, mirroring Nest's @Header() and @Redirect() decorators while
keeping the behavior adapter-neutral:
use ;
The macro form is available on controller route methods:
# use Result;
;
GET and DELETE helpers can also serialize response DTOs while still exposing the request for params, query values, and headers:
use ;
use Serialize;
Serialization
Boot serialization mirrors Nest's ClassSerializerInterceptor and
@SerializeOptions shape, but works on adapter-neutral BootResponse values.
Register SerializationInterceptor globally, then attach
SerializationOptions to routes or controllers. The interceptor only rewrites
JSON response bodies; text, raw bytes, redirects, empty responses, and SSE
streams pass through unchanged.
use ;
use json;
let app = builder
.use_global_serialization
.route
.build?;
Controller-level serialization options are inherited by routes that do not set their own options:
use ;
use json;
The macro form mirrors Nest's @SerializeOptions(). Use include, exclude,
and skip_null on a controller impl or on an individual route:
use ;
use ;
;
Use SerializationInterceptor::with_options(...) when the same default policy
should apply to every JSON response, even routes without explicit metadata:
use ;
let app = builder
.use_global_interceptor
.build?;
include_fields(...) keeps only the named top-level fields on a JSON object or
each object in a top-level JSON array. exclude_fields(...) removes named
top-level fields, and skip_null_fields() drops null top-level fields. When a
serialized response had a Content-Length, Boot updates it after rewriting the
body.
Compression
Enable the compression feature to use CompressionInterceptor, a Nest-style
response compression hook for gzip. It runs after handlers and other route
interceptors, checks the request Accept-Encoding header, skips responses that
are already encoded or too small, and leaves SSE streams unchanged.
use ;
Clients opt in with Accept-Encoding: gzip. Compressed responses include
Content-Encoding: gzip and Vary: accept-encoding. If the response had a
Content-Length, Boot rewrites it to the compressed body length.
File Upload
Enable the file-upload feature to parse multipart/form-data bodies from
BootRequest. This mirrors Nest's file upload workflow while keeping the core
adapter-neutral: adapters only need to preserve the request body and headers.
use ;
MultipartForm separates text fields from files. Use field(...),
field_values(...), file(...), and files_by_name(...) for lookup.
MultipartOptions can limit total body size, per-field size, per-file size,
field count, and file count. Non-multipart requests return
BootError::UnsupportedMediaType; malformed multipart bodies and invalid text
fields return BootError::BadRequest; limit failures return
BootError::PayloadTooLarge.
Static Assets
Enable the static feature to import StaticModule, Boot's module-level
equivalent of Nest's ServeStaticModule. It registers provider-backed GET and
HEAD catch-all routes under a configured serve root and reads files through
tokio::fs.
use ;
StaticModule::new("static", "public").with_serve_root("/assets") serves
public/app.css at /assets/app.css. Directory requests serve index.html
when it exists. with_fallback_file("index.html") enables SPA fallback for
missing client-side routes. Dotfiles are not served by default, and paths that
try to escape the configured root return BootError::Forbidden.
Authentication
Enable the auth feature to register provider-backed authentication strategies
and protect routes with AuthGuard, similar to Nest's AuthGuard("jwt")
pattern. AuthModule exports AuthService; AuthGuard resolves that provider
from the current module scope, runs the selected strategy, checks optional
roles/scopes metadata, and attaches an AuthPrincipal to BootRequest.
use ;
use json;
Use AuthModule::bearer(...) for JWT or opaque bearer-token verification.
Use AuthModule::strategy("api-key", ...) plus route metadata
AUTH_STRATEGY_METADATA when a route needs a named strategy. Controller and
route macros can attach the same metadata with
#[metadata("auth.public", true)], #[metadata("auth.roles", ["admin"])],
#[metadata("auth.scopes", ["cats:read"])], and
#[metadata("auth.strategy", "api-key")].
Security Helpers
Enable the security feature to use Nest-style security hooks through the
same middleware, guard, and interceptor pipeline as the rest of Boot.
use_global_cors(...) registers CORS response headers and generated hidden
OPTIONS preflight routes for known route shapes. use_global_security_headers
adds helmet-like response headers when handlers have not set them. CSRF and
rate limiting are ordinary guards, so they can be global, controller-level, or
route-level.
use ;
use Duration;
CSRF checks protect POST, PUT, PATCH, and DELETE by default. The guard
compares the x-csrf-token header with the csrf-token cookie and returns
HTTP 403 for missing or mismatched tokens. The rate limit guard uses an
in-memory fixed window and returns HTTP 429 after the configured request count;
use it for local services, tests, and adapter-neutral policy wiring before
plugging in a distributed backend.
Sessions
Enable the session feature to use provider-backed sessions, similar to Nest's
session middleware setup. use_global_session_module(...) imports the session
provider, runs SessionMiddleware before handlers, and runs
SessionCookieInterceptor after handlers so a session cookie is only written
when session data exists.
use ;
use Duration;
SessionOptions controls the cookie name, TTL, path, domain, HttpOnly,
Secure, SameSite, and rolling-cookie behavior. The default store is
in-memory for tests and single-process services; production adapters can provide
a custom SessionStore.
Params And Query
Boot keeps route params adapter-neutral. Use whole {name} segments in routes
and read decoded values from BootRequest one at a time or as a typed DTO; use
a final {*path} segment for catch-all routes that capture zero or more
trailing segments. Query strings can be read as raw single values, repeated
values, or decoded into a typed DTO. Parameter names must be non-empty,
well-formed, and unique after controller and global prefixes are applied. Route
definitions and prefixes are path-only and reject query or fragment markers;
read query values from the request instead. Invalid percent encoding and
invalid UTF-8 in decoded params or query values map to BootError::BadRequest.
Prefer query_value(...) and query_values(...) when the handler should reject
malformed query strings; use query_pairs(...) when the handler or adapter
needs every decoded query pair, including repeated keys.
Route definitions can also be inspected without executing handlers via
matches_path(...), path_params(...), path_shape(...), and
path_param_names(...).
use ;
use Deserialize;
Global Prefix And Headers
Use an application prefix when an adapter should expose every route under a shared base path:
use ;
Header helpers normalize names for storage and lookup:
use ;
use Duration;
let request = new
.with_content_type
.with_body
.with_content_length
.with_header
.with_header
.append_header
.append_header;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
request.validate_headers.unwrap;
request.validate_content_length.unwrap;
request.validate_body_limit.unwrap;
request.validate.unwrap;
request.validate_with_body_limit.unwrap;
assert!;
assert!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
let response = new
.with_content_type
.with_content_length
.with_location
.with_cookie
.unwrap
.with_cookie
.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
response.validate_content_length.unwrap;
assert_eq!;
assert!;
let unauthorized = empty
.with_www_authenticate
.append_www_authenticate;
assert_eq!;
assert_eq!;
let logout = no_content
.delete_cookie
.unwrap;
assert_eq!;
The Axum adapter rejects request header values that cannot be represented as
text and reports them as BootError::BadRequest; invalid Content-Length
headers are also rejected as bad requests, repeated Content-Length values
must agree, declared lengths must match the decoded body length, and values
above the configured body limit map to HTTP 413 before the body is read.
The same request header, strict repeated-header, and body-length checks are
available in core via BootRequest::validate_headers(),
BootRequest::strict_content_length(), BootRequest::validate_content_length(),
BootRequest::validate_body_limit(...), BootRequest::validate(), and
BootRequest::validate_with_body_limit(...). Use validate() for header and
Content-Length checks, or validate_with_body_limit(...) when an adapter also
needs body-limit enforcement after reading the body. Use header_entries() on
requests and responses when an adapter needs to forward every stored header
line, including appended repeated headers. Request and response accessors such
as method(), path(), status(), body(), and into_body() let adapters
read common fields without depending on struct fields directly. Response-side
checks are available through BootResponse::strict_content_length() and
BootResponse::validate_content_length().
Invalid response status codes or headers are reported as internal adapter
errors instead of being silently dropped; adapters can reuse
BootResponse::validate() for the same status-code, Content-Length, no-body
status, and response header checks. Response Content-Length values must also
be valid, consistent, and match the response body length, and statuses that
cannot carry a body reject non-empty response bodies. Unsupported HTTP
methods are rejected as method-not-allowed errors instead of being remapped to
GET.
Replace The HTTP Backend
The core crate depends on the HttpAdapter trait, not on Axum. Axum is the
first adapter because it is a strong default for async Rust services, but a
Boot application can be served by any backend that can translate Boot routes,
requests, and responses.
Disable the default adapter when you only want the framework-neutral core:
[]
= { = "0.1", = false }
Implement an adapter for another HTTP stack, test harness, in-process gateway,
or custom runtime. In-process callers can also dispatch through the resolved
route table with BootApplication::call(...) or handle(...), reusing route
matching, parameter decoding, pipeline hooks, and exception filters. call(...)
returns unhandled BootErrors while handle(...) converts them to
BootResponse::from_error(...). Individual route snapshots expose the same
call(...) and handle(...) split for direct dispatch after an adapter has
selected a route. Custom adapters can
also query BootApplication::route_for(...), route_match(...), and
BootApplication::allowed_methods(...) from the same most-specific path
matching rules. allowed_methods_header(...) returns the corresponding
comma-separated Allow header value when a path matches. Adapters can build
method-not-allowed responses and use
BootResponse::from_error(...) or BootError::http_status_code(...) plus
http_response_message(...) for consistent error responses. Route snapshots
also expose resolved path shape, path parameter names, module metadata, and
controller metadata for adapter registration, logging, and diagnostics. When an
adapter has an actual request path, route_match(...) returns the selected
route plus decoded path parameter values with the same bad-request semantics as
route execution:
use SocketAddr;
use ;
;
Design Direction
A3S Boot aims to provide a structured service framework for A3S components:
| Concept | Direction |
|---|---|
| Module | A named feature boundary with imports, providers, and routes |
| ModuleRef | Typed provider container used by controllers and hosts |
| Testing module | Test-only module compilation with provider overrides and in-process calls |
| Discovery/Reflector | Runtime snapshots and metadata lookup for modules, routes, gateways, and message patterns |
| HTTP adapter | Replaceable backend adapter; Axum is the first implementation |
| Controller | Typed request handlers grouped by route prefix |
| Provider | Injectable service or repository dependency |
| Middleware | Request inspection, mutation, and short-circuiting before guards and pipes |
| Guard | Request authorization and policy gate |
| Interceptor | Cross-cutting request/response behavior at global, controller, or route scope |
| Pipe | Request validation and transformation |
| WebSocket gateway | Event-based bidirectional message handlers |
| Message transport | Adapter-neutral request-response and event-only message patterns |
| Event emitter | Optional provider-backed in-process application events |
| CQRS | Optional command, query, and event buses through CqrsModule |
| Authentication | Optional strategy-backed auth provider and AuthGuard |
| Health check | Optional provider-backed readiness/liveness reports |
| Configuration | Optional ACL-backed typed providers through ConfigModule |
| Database | Optional provider-backed database facade through DatabaseModule |
| HTTP client | Optional provider-backed outbound HTTP client through HttpModule |
| Cache | Optional typed cache provider through CacheModule |
| Scheduler | Optional provider-backed task scheduling through ScheduleModule |
| Queue | Optional provider-backed background jobs through QueueModule |
| Logger | Optional provider-backed structured logging through LoggingModule |
| Compression | Optional gzip response compression through CompressionInterceptor |
| File upload | Optional multipart form parsing through BootRequest helpers |
| Static assets | Optional provider-backed static file module for assets and SPA fallback |
| Security | Optional CORS, security headers, CSRF, and rate limiting helpers |
| Session | Optional provider-backed session store, middleware, and cookie persistence |
| Response cookies | Typed Set-Cookie and delete-cookie helpers on BootResponse |
| API versioning | URI, header, or media type version matching through route metadata |
| Serialization | JSON response shaping through SerializationInterceptor metadata |
| Filter | Error mapping into HTTP responses |
| Lifecycle hook | Startup and shutdown behavior for modules and providers |
The design is intentionally progressive: a small service can start with direct routes, then move into modules, providers, controllers, and request pipelines as the codebase grows. The framework core remains independent from any specific HTTP backend so A3S Gateway, A3S Code services, and standalone control-plane APIs can choose their adapter.
Source Layout
The crate is split by framework concern:
src/
├── adapters/ # Optional backend adapters such as Axum
├── app/ # Application instance, builder, and module registration
├── auth.rs # Optional strategy-backed authentication module and guard
├── cache.rs # Optional typed cache module and in-memory store
├── compression.rs # Optional gzip response compression interceptor
├── config.rs # Optional ACL-backed typed configuration module
├── cqrs.rs # Optional command, query, and event buses
├── database.rs # Optional provider-backed database facade and backend traits
├── discovery.rs # Runtime discovery snapshots and metadata reflector
├── events.rs # Optional in-process application event emitter module
├── health.rs # Optional provider-backed health check module
├── http/ # Adapter-neutral request, response, methods, and query parsing
├── http_client.rs # Optional provider-backed outbound HTTP client module
├── logging.rs # Optional provider-backed structured logging module
├── module/ # Module trait, dynamic modules, exports, and lifecycle hooks
├── pipeline/ # Middleware, pipes, guards, interceptors, filters, and execution context
├── provider/ # Provider tokens, definitions, and ModuleRef container
├── queue.rs # Optional provider-backed queue and in-process backend
├── routing/ # Route handlers, controllers, route execution, and path matching
├── schedule.rs # Optional provider-backed scheduler and in-process backend
├── security.rs # Optional CORS, security headers, CSRF, and rate limiting helpers
├── serialization.rs # Adapter-neutral JSON response shaping interceptor
├── session.rs # Optional provider-backed session store and cookie pipeline
├── static_files.rs # Optional provider-backed static file module
├── testing.rs # Nest-style test module builder and compiled testing module
├── transport/ # Adapter-neutral microservice message patterns and transports
├── validation.rs # DTO validation trait and route validation hooks
├── versioning.rs # Adapter-neutral API versioning strategies and route metadata
├── websocket/ # Adapter-neutral WebSocket gateways, messages, and pipeline hooks
├── error.rs
├── file_upload.rs # Optional multipart form and file upload helpers
└── lib.rs
lib.rs only exports the public surface. Behavior tests live under tests/
and exercise the crate through public APIs.
Development
License
MIT