Configuration ownership first
The most important architecture decision is where metadata lives. HttpServer reads HttpClientModule metadata from the class referenced in defineHttp({ module }), so that class becomes the single owner of every controller, service, guard, and middleware declaration in the HTTP surface. Using AppModule as that class keeps ownership explicit and prevents silent configuration drift when teams refactor modules.
No local HTTP wrapper module needed
Avoid creating a separate AppHttpModule only to host HttpClientModule metadata. Keep HttpClientModule on AppModule, import HttpModule in AppModule, and reference AppModule in defineHttp. A second module with its own HttpClientModule is silently ignored by the server because only the root reference is resolved.
This model removes ownership ambiguity. Server lifecycle, guards, middleware, and route registration all point to one root class and one plugin entry. When you add a new controller or change the guard stack, the change is visible in one place and takes effect across the entire API surface without touching configuration files.
HttpConfig contract
HttpConfig is the interface that defineHttp() accepts and ConfigStore delivers to HttpServer at boot time. Every field maps to a specific runtime behavior, so understanding the contract prevents surprises during deployment.
SecurityConfig substructure
The security field accepts a SecurityConfig object with four optional subfields. Each subfield controls one layer of the hardening stack and can be set to false to disable it entirely. When a subfield is omitted, the package applies SECURITY_DEFAULTS automatically.
Recommended integration split
Keep the defineHttp call in a dedicated config file so network and security decisions are visible without scrolling through module decorators. The config file imports AppModule, and spraxium.config.ts imports the config file. This three-file pattern gives each concern a stable home: the config file owns infrastructure, the module owns metadata, and the root config owns plugin registration.
import { defineHttp } from '@spraxium/http';
import { AppModule } from '../src/app.module';
export const httpPlugin = defineHttp({
enabled: true,
host: process.env.HTTP_HOST ?? '0.0.0.0',
port: Number(process.env.HTTP_PORT ?? 3000),
apiKey: process.env.HTTP_API_KEY ?? '',
sharding: false,
security: {
hidePoweredBy: true,
headers: {
frameOptions: 'DENY',
contentSecurityPolicy: "default-src 'none'",
referrerPolicy: 'no-referrer',
},
cors: {
origins: ['https://admin.example.com'],
methods: ['GET', 'POST', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Api-Key'],
credentials: true,
},
bodyLimit: { maxSize: 256_000 },
},
module: AppModule,
});HttpClientModule metadata shape
@HttpClientModule is a class decorator that stores metadata consumed by HttpServer during route registration. Each field controls a different phase of the startup pipeline.
The split between middleware and middlewareProviders gives clear ownership between global pipeline policy and route-level declarations. Global middleware runs on every request unconditionally. Route middleware runs only on endpoints decorated with @HttpUseMiddleware and is resolved lazily from the provider map, so unreferenced middleware classes never execute.
Setup sequence
Extract plugin configuration into a dedicated file
Keep the defineHttp call in config/http.config.ts so network binding,
security policy, and sharding mode are visible in one place. This also keeps
spraxium.config.ts focused on plugin registration instead of
implementation details. Environment variables for host, port, and API key
should be read here.
Point module to your real composition root
Set module: AppModule in defineHttp so HttpServer reads metadata from
the same class that owns the module graph. Creating a separate HttpAppModule
leads to drift because changes in AppModule do not propagate to the HTTP
metadata source automatically.
Import HttpModule once in AppModule
HttpModule is global and should appear once in the root module imports. That ensures one lifecycle owner for server startup and graceful shutdown. Importing it in multiple modules does not cause errors but adds no benefit.
Register guards and middleware in HttpClientModule
Declare custom guard classes in the @HttpClientModule() array for global protection. The framework resolves each guard class via DI at startup. Middleware instances go in middleware and run after the security stack. Route-level middleware goes in middlewareProviders and is activated per-endpoint with @HttpUseMiddleware.
Register one HTTP plugin entry in defineConfig
Keep a single HTTP plugin entry inside defineConfig({plugins}). Multiple
entries lead to ambiguous ownership and unstable route registration because
ConfigStore returns only one config for the defineHttp key.
Startup behavior by mode
| Mode | When it starts | Client requirement | Bridge implementation |
|---|---|---|---|
| Direct, with sharding: false | During onBoot lifecycle hook, after the Discord Client is ready. | Required. If missing, server logs an error and skips startup. | DirectBotBridge: accesses guild cache and Discord API through the client instance directly. |
| Sharded, with sharding: true | Via ParentHookRegistry when the parent process creates the ShardingManager. | Not required. The ShardingManager replaces the client for bot interactions. | ShardedBotBridge: uses |
In direct mode, the server has immediate access to the Discord Client and can inject it into the DI map alongside BotBridge. Controllers that need the client directly (for features not covered by the bridge) can declare it as a constructor parameter. In sharded mode, the client is not available in the HTTP process, so controllers must use BotBridge exclusively for Discord interactions.