Platform agnosticism
The underlying idea is that the application's logic (modules, providers, guards, interceptors) doesn't depend on how the data arrives. In Corvis this holds to a large extent across transports (HTTP, WebSockets, server-less context), but not across HTTP servers: there is only one.
A single HTTP server
Corvis ships its own server on top of hyper and Tokio. Its behavior reproduces that of an Express 5 application: the body parser, the router, the default headers, ETag, CORS, and the res object follow that semantics (see HTTP adapters).
- There are no interchangeable adapters: you can't switch to another HTTP server without changing the framework.
HttpAdapterHostexists and is injected into any provider, but it gives access to Corvis's server, not to an abstraction over several.http_adapter().get_type()returns"corvis".- What you can do at a low level is register listeners for the
upgradeevent withget_http_server().on_upgrade(...)(see WebSockets: adapters).
What is transport-independent
Providers and modules
Providers know nothing about HTTP. The same module graph works in:
- an HTTP application (
CorvisFactory::create), - a server-less context for scripts, CLIs, and workers (
CorvisFactory::create_application_context, see Standalone applications), - a testing module that serves requests in memory, without sockets (see Testing).
Enhancers shared between HTTP and WebSockets
Guards, interceptors, and pipes use the same traits in controllers and in gateways. The ExecutionContext indicates the transport with get_type(), so the same piece can serve both. In examples/tertulia, AuthGuard protects REST routes and chat messages:
#[async_trait]
impl CanActivate for AuthGuard {
async fn can_activate(&self, ctx: &ExecutionContext) -> Result<bool, HttpException> {
if ctx.get_type() == ContextType::Ws {
return match ChatUser::of(ctx) {
Some(_) => Ok(true),
None => Err(WsException::new("Unauthorized")),
};
}
let request = ctx.switch_to_http().get_request();
let token = extract_bearer_token(request).ok_or_else(|| UnauthorizedException::new("Missing bearer token"))?;
let user: ChatUser = self.jwt.verify_or_unauthorized(&token)?;
request.insert(user);
Ok(true)
}
}// In a controller:
#[post]
#[use_guards(AuthGuard)]
async fn create(&self, #[body] dto: CreateRoomDto, user: ChatUser) -> HttpResult<Room> { ... }
// In a gateway:
#[websocket_gateway(path = "/chat")]
#[use_guards(AuthGuard)]
#[use_interceptors(WsLoggingInterceptor)]
#[use_pipes(ZodValidationPipe::auto())]
pub struct ChatGateway { ... }RolesGuard, in the same example, reads metadata with Reflector and works the same on a route with #[set_metadata(ROLES_KEY, [MODERATOR])] as on a #[subscribe_message] with the same attribute.
DTOs with a schema (zod_dto!) and ZodValidationPipe are also reused: the chat gateway validates its messages with the same schemas a controller would use.
Limitations
- A single HTTP server, as explained above.
- No microservices:
ContextTypehas anRpcvariant, but no transport produces it. See Microservices. - Global enhancers:
use_global_*andProvider::app_*only apply to HTTP routes, not to gateways. Declare the gateway's enhancers on the gateway itself. - Exception filters: they don't apply to gateways (see WebSockets: exception filters).