@nextrush/coreCore
Application class, middleware composition, and the extension system
The core package provides the foundation of every NextRush application: the Application class, middleware composition, and the extension system.
Source & internals
$ pnpm add @nextrush/core
Usually Included
Most users install nextrush which includes this package. Install @nextrush/core directly only
when using non-Node.js runtimes or building custom tooling.
What It Provides
// Application
import { Application, createApp } from '@nextrush/core';
import type {
ApplicationOptions,
ErrorHandler,
ListenCallback,
Logger,
Routable,
} from '@nextrush/core';
// Middleware utilities
import { compose, isMiddleware, flattenMiddleware } from '@nextrush/core';
import type { ComposedMiddleware } from '@nextrush/core';
// Error classes (re-exported from @nextrush/errors)
import {
HttpError,
NotFoundError,
BadRequestError,
UnauthorizedError,
ForbiddenError,
InternalServerError,
NextRushError,
} from '@nextrush/core';
// Re-exported types for convenience
import type {
Context,
Middleware,
Extension,
ExtensionContext,
Next,
HttpMethod,
HttpStatusCode,
RouteHandler,
RouteParams,
QueryParams,
ContextState,
} from '@nextrush/core';
// Re-exported constants
import { HttpStatus, ContentType } from '@nextrush/core';Application
The Application class is the container for your middleware stack and extensions.
Creating an Application
import { createApp } from '@nextrush/core';
// Default options
const app = createApp();
// With options
const app = createApp({
env: 'production',
proxy: true,
});Application Options
ApplicationOptions
| Property | Type | Description |
|---|---|---|
env | 'development' | 'production' | 'test'= 'development' | Environment mode |
proxy | boolean= false | Trust proxy headers (X-Forwarded-For, etc.) |
logger? | Logger | Pluggable logger instance. Defaults to a silent no-op logger. Pass console for quick development logging. |
router? | Router | A router the app owns. Route methods (app.get, app.post, …) delegate to it. The `nextrush` meta-package's createApp() injects one automatically. |
container? | Container | A per-app DI container, exposed to extensions and registrars (e.g. registerControllers). Injected automatically by `nextrush/class`. |
Environment Mode
// Development: request errors logged server-side via app.logger
const app = createApp({ env: 'development' });
// Production: request errors not logged; responses never leak details
const app = createApp({ env: 'production' });
// Test: for automated testing
const app = createApp({ env: 'test' });Logger
The logger option accepts any object implementing the Logger interface:
Logger
| Property | Type | Description |
|---|---|---|
error | (...args: unknown[]) => void | Log error-level messages |
warn | (...args: unknown[]) => void | Log warning-level messages |
info | (...args: unknown[]) => void | Log informational messages |
debug | (...args: unknown[]) => void | Log debug-level messages |
Any object with these methods works, including console and structured loggers like pino.
// Quick development logging
const app = createApp({ logger: console });
// Production structured logger (e.g. pino)
const app = createApp({ logger: pino() });The logger is available on app.logger and is used internally by the error handler and extension setup/destroy hooks.
Proxy Mode
Enable when behind a reverse proxy (nginx, AWS ALB, Cloudflare):
const app = createApp({ proxy: true });Middleware Registration
Basic Usage
// Single middleware
app.use(async (ctx) => {
console.log(`${ctx.method} ${ctx.path}`);
await ctx.next();
});
// Multiple middleware at once
app.use(cors(), helmet(), json());
// Method chaining
app.use(cors()).use(helmet()).use(json());Router Composition
Mount routers directly on the application using app.route() — Hono-style composition:
import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';
const app = createApp();
// Create feature routers
const users = createRouter();
users.get('/', (ctx) => ctx.json([]));
users.get('/:id', (ctx) => ctx.json({ id: ctx.params.id }));
const posts = createRouter();
posts.get('/', (ctx) => ctx.json([]));
// Mount directly — clean like Hono!
app.route('/api/users', users);
app.route('/api/posts', posts);Benefits over Classic Pattern
| Classic Pattern | Hono-Style Composition |
|---|---|
router.use('/users', usersRouter) then app.use(router.routes()) | app.route('/users', users) |
| Requires main router | Direct mounting |
Extra .routes() call | No extra calls |
Classic Pattern Still Works
const router = createRouter();
router.use('/users', usersRouter);
router.use('/posts', postsRouter);
app.route('/', router);Two Syntax Styles
Both work identically—use whichever you prefer:
// Modern: ctx.next()
app.use(async (ctx) => {
console.log('Before');
await ctx.next();
console.log('After');
});
// Koa-style: (ctx, next) parameter
app.use(async (ctx, next) => {
console.log('Before');
await next();
console.log('After');
});Execution Order
Middleware runs in registration order following the onion model — code before ctx.next() runs on the way in, code after it runs on the way out. See Middleware → the onion model for the full walkthrough and diagram.
Error Handling
Custom Error Handler
Set a custom error handler with setErrorHandler(). This replaces any previously set handler — only one is active at a time.
app.setErrorHandler((error, ctx) => {
console.error('Request failed:', error);
if ('status' in error && typeof error.status === 'number') {
ctx.status = error.status;
} else {
ctx.status = 500;
}
ctx.json({
error: error.message,
...(app.options.env === 'development' && { stack: error.stack }),
});
});There is no onError() method — setErrorHandler() is the only way to set the application error handler; the name makes the replacement semantics clear (calling it again replaces the previous handler).
Default Error Behavior
Without a custom handler, the built-in handler decides what the client sees from each error's expose flag — not from the environment. HttpError/NextRushError set expose: true for 4xx and false for 5xx; a plain Error never exposes.
| Error kind | Client message | Stack in response | Server-side log |
|---|---|---|---|
4xx (expose: true) | The error's message | Never | development / test |
5xx / plain Error | "Internal Server Error" | Never | development / test |
The client response is identical in every environment — a 500 never leaks its message, even in development. The only environment-dependent behavior is the server-side log: the default handler logs via app.logger in development and test, and stays silent in production. To include a stack trace or the raw message in responses, set a custom handler (above). See Application → error handling for the rationale.
Extension System
Extensions are the rare (~0.1%) long-lived, app-scoped services — an event bus, a database pool — that need a boot phase and a teardown phase. Most capability is middleware (app.use()), not an Extension. See Extending NextRush for the full taxonomy (Middleware, Registrar, Extension) and when to reach for each.
Registering Extensions
app.extend() is synchronous and only queues the extension — setup() runs later, at app.ready(), in registration order:
import { events } from '@nextrush/events';
app.extend(events());
await app.ready(); // adapters call this automatically before start()
app.events.emit('server:started', {});Extension Lifecycle
// app.ready() boots every registered extension once, in order
await app.ready();
// After ready(), configuration is frozen
app.use(mw); // ❌ throws — use()/route()/extend() all throw after ready()app.close() tears extensions down in reverse registration order, using Promise.allSettled so one failing destroy() never strands the others.
Checking for a Decoration
There is no public app.decorate() — only ctx.decorate() inside an Extension's setup(). The only public, read-only check from outside an extension is:
if (app.hasDecorator('events')) {
console.log('events extension is active');
}Middleware Composition
compose()
Combine multiple middleware into a single function:
import { compose } from '@nextrush/core';
const security = compose([cors(), helmet(), rateLimit({ max: 100 })]);
// Use as single middleware
app.use(security);Utility Functions
import { isMiddleware, flattenMiddleware } from '@nextrush/core';
// Type guard
if (isMiddleware(fn)) {
app.use(fn);
}
// Flatten nested arrays
const flat = flattenMiddleware([mw1, [mw2, mw3], [[mw4]]]);
// Result: [mw1, mw2, mw3, mw4]Application Lifecycle
Request Handler
Adapters use app.callback() to get the request handler:
const handler = app.callback();
// Handler signature: (ctx: Context) => Promise<void>Starting
Adapters call app.ready() (booting extensions) then app.start() when the server begins:
await app.ready();
app.start();
console.log(app.isRunning); // trueGraceful Shutdown
app.close() returns Promise<Error[]> — an array of errors from extensions that failed to destroy. An empty array means all extensions shut down cleanly.
const errors = await app.close();
// What happens:
// 1. Sets isRunning = false
// 2. Calls destroy() on extensions (reverse registration order)
// 3. Collects errors via Promise.allSettled (all extensions get a chance to destroy)
// 4. Clears decorations and the extension registry
// 5. Returns any extension destroy errorsExample shutdown hook:
process.on('SIGTERM', async () => {
console.log('Shutting down...');
const errors = await app.close();
if (errors.length > 0) {
console.error('Extension shutdown errors:', errors);
}
process.exit(errors.length > 0 ? 1 : 0);
});Application Properties
app.isProduction; // boolean — true if env === 'production'
app.isRunning; // boolean — true after start() called
app.middlewareCount; // number — count of registered middleware
app.options; // ApplicationOptions — readonly config
app.logger; // Logger — configured logger instance (readonly)Complete Example
import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';
import { serve } from '@nextrush/adapter-node';
const app = createApp({
env: 'production',
proxy: true,
logger: console,
});
// Error handler
app.setErrorHandler((error, ctx) => {
app.logger.error(error);
if ('status' in error && typeof error.status === 'number') {
ctx.status = error.status;
} else {
ctx.status = 500;
}
ctx.json({ error: app.isProduction ? 'Internal Server Error' : error.message });
});
// Middleware
app.use(async (ctx) => {
const start = Date.now();
await ctx.next();
app.logger.info(`${ctx.method} ${ctx.path} - ${Date.now() - start}ms`);
});
// Feature routers
const health = createRouter();
health.get('/', (ctx) => ctx.json({ status: 'healthy' }));
const users = createRouter();
users.get('/', (ctx) => ctx.json([]));
users.get('/:id', (ctx) => ctx.json({ id: ctx.params.id }));
// Mount routers — Hono-style composition
app.route('/health', health);
app.route('/users', users);
// Start server
serve(app, { port: 8080 });TypeScript Types
import { createApp, Application } from '@nextrush/core';
import type {
ApplicationOptions,
ErrorHandler,
ListenCallback,
Logger,
Routable,
ComposedMiddleware,
} from '@nextrush/core';
// Type the application
const app: Application = createApp();
// Type the error handler
const errorHandler: ErrorHandler = (error, ctx) => {
ctx.status = 500;
ctx.json({ error: error.message });
};
app.setErrorHandler(errorHandler);API Reference
Application Class
| Property/Method | Type | Description |
|---|---|---|
app.use(...mw) | this | Register one or more middleware functions (chainable) |
app.route(path, router) | this | Mount a Routable at a path prefix (Hono-style) |
app.setErrorHandler(handler) | this | Set the application error handler (replaces any previous handler) |
app.extend(extension) | this | Queue an extension. setup() runs later, at ready(). |
app.ready() | Promise<this> | Boot every registered extension's setup() once, in order. Idempotent. Freezes configuration. |
app.hasDecorator(name) | boolean | Check whether a name has been decorated onto the app by an extension |
app.callback() | (ctx: Context) => Promise<void> | Build the request handler. Snapshots the middleware stack at call time. |
app.start() | void | Mark app as running. Call after ready() — use(), route(), extend() already throw post-ready(). |
app.close() | Promise<Error[]> | Graceful shutdown. Destroys extensions in reverse order. Returns errors from failed extension destroys. |
app.isProduction | boolean | true when env === 'production' |
app.isRunning | boolean | true after start(), false after close() |
app.isReady | boolean | true after ready() has booted all extensions |
app.middlewareCount | number | Count of registered middleware |
app.extensionCount | number | Count of registered extensions |
app.options | ApplicationOptions | Readonly configuration |
app.logger | Logger | Readonly logger instance |
app.router | Router | undefined | The app-owned router, if one was configured |
app.container | Container | undefined | The app-owned DI container, if one was configured |
compose()
function compose(middleware: Middleware[]): ComposedMiddleware;Composes multiple middleware into a single function using the onion model.
isMiddleware()
function isMiddleware(fn: unknown): fn is Middleware;Type guard to check if a value is a valid middleware function.
flattenMiddleware()
function flattenMiddleware(arr: (Middleware | Middleware[])[]): Middleware[];Flattens nested middleware arrays into a single array.