Framework Internals
This section covers the internal architecture patterns that power Stacks.js applications.
Middleware System
Middleware provides a mechanism for filtering and modifying HTTP requests and responses as they flow through your application.
Defining Middleware
// app/Middleware/AuthMiddleware.ts
import { Middleware, Request, Response, Next } from '@stacksjs/types'
export default class AuthMiddleware implements Middleware {
/**
* Handle the incoming request.
*/
async handle(request: Request, next: Next): Promise<Response> {
// Before: Check authentication
if (!request.user) {
return Response.redirect('/login')
}
// Call next middleware in the stack
const response = await next(request)
// After: Modify response if needed
response.headers.set('X-Authenticated', 'true')
return response
}
}
Middleware Types
// Terminating middleware (doesn't call next)
export class MaintenanceMiddleware implements Middleware {
async handle(request: Request, next: Next): Promise<Response> {
if (config.app.maintenance) {
return Response.view('maintenance', {}, 503)
}
return next(request)
}
}
// Transforming middleware (modifies request/response)
export class JsonMiddleware implements Middleware {
async handle(request: Request, next: Next): Promise<Response> {
// Transform request
if (request.is('application/json')) {
request.parsedBody = await request.json()
}
const response = await next(request)
// Transform response
if (response.data && !response.headers.has('Content-Type')) {
return Response.json(response.data)
}
return response
}
}
// Measuring middleware (observes without modifying)
export class TimingMiddleware implements Middleware {
async handle(request: Request, next: Next): Promise<Response> {
const start = performance.now()
const response = await next(request)
const duration = performance.now() - start
response.headers.set('X-Response-Time', `${duration.toFixed(2)}ms`)
return response
}
}
Registering Middleware
// config/middleware.ts
export default {
// Global middleware (runs on every request)
global: [
'TimingMiddleware',
'CorsMiddleware',
'JsonMiddleware',
],
// Route middleware (applied via groups or routes)
route: {
auth: 'AuthMiddleware',
guest: 'GuestMiddleware',
admin: 'AdminMiddleware',
verified: 'VerifiedEmailMiddleware',
throttle: 'ThrottleMiddleware',
signed: 'SignedUrlMiddleware',
},
// Middleware groups
groups: {
web: [
'SessionMiddleware',
'CsrfMiddleware',
'ShareErrorsFromSession',
],
api: [
'ThrottleMiddleware:60,1',
'JsonMiddleware',
],
},
// Middleware priority (execution order)
priority: [
'MaintenanceMiddleware',
'TimingMiddleware',
'CorsMiddleware',
'SessionMiddleware',
'AuthMiddleware',
],
}
Applying Middleware to Routes
// routes/web.ts
import { Router } from '@stacksjs/router'
// Single middleware
Router.get('/dashboard', 'DashboardController@index')
.middleware('auth')
// Multiple middleware
Router.get('/admin', 'AdminController@index')
.middleware(['auth', 'admin', 'verified'])
// Middleware with parameters
Router.post('/api/upload', 'UploadController@store')
.middleware('throttle:10,1') // 10 requests per minute
// Middleware groups
Router.group({ middleware: 'web' }, () => {
Router.get('/', 'HomeController@index')
Router.get('/about', 'PageController@about')
})
// Excluding middleware
Router.get('/health', 'HealthController@check')
.withoutMiddleware('auth')
Middleware Parameters
// app/Middleware/RoleMiddleware.ts
export default class RoleMiddleware implements Middleware {
async handle(request: Request, next: Next, ...roles: string[]): Promise<Response> {
const user = request.user
if (!user || !roles.some(role => user.hasRole(role))) {
return Response.json({ error: 'Unauthorized' }, 403)
}
return next(request)
}
}
// Usage in routes
Router.get('/admin', 'AdminController@index')
.middleware('role:admin,super-admin')
Event System
Stacks implements an observer pattern for decoupled event-driven architecture.
Defining Events
// app/Events/UserRegistered.ts
import { Event } from '@stacksjs/events'
export default class UserRegistered extends Event {
constructor(public user: User) {
super()
}
// Optional: broadcast to websocket channels
broadcastOn(): string[] {
return [
'users', // Public channel
`private-user.${this.user.id}`, // Private channel
]
}
// Optional: custom event name (defaults to class name)
broadcastAs(): string {
return 'user.registered'
}
// Optional: custom broadcast data (defaults to public properties)
broadcastWith(): object {
return {
id: this.user.id,
name: this.user.name,
email: this.user.email,
}
}
// Optional: condition for broadcasting
broadcastWhen(): boolean {
return this.user.isActive
}
}
Defining Listeners
// app/Listeners/SendWelcomeEmail.ts
import { Listener } from '@stacksjs/events'
import { UserRegistered } from '@/Events/UserRegistered'
export default class SendWelcomeEmail extends Listener {
/**
* Handle the event.
*/
async handle(event: UserRegistered): Promise<void> {
await Mail.send({
to: event.user.email,
template: 'welcome',
data: { name: event.user.name },
})
}
/**
* Handle a job failure.
*/
async failed(event: UserRegistered, error: Error): Promise<void> {
log.error('Failed to send welcome email', {
userId: event.user.id,
error: error.message,
})
}
}
// Queueable listener
export default class ProcessUserAvatar extends Listener {
// Process in background queue
queue = 'default'
connection = 'redis'
delay = 0
tries = 3
async handle(event: UserRegistered): Promise<void> {
await ImageProcessor.generateAvatarVariants(event.user.avatarUrl)
}
}
Registering Events & Listeners
// config/events.ts
export default {
// Event to listener mapping
listen: {
UserRegistered: [
'SendWelcomeEmail',
'CreateDefaultSettings',
'ProcessUserAvatar',
'NotifyAdmins',
],
OrderPlaced: [
'SendOrderConfirmation',
'UpdateInventory',
'NotifyWarehouse',
],
PaymentFailed: [
'SendPaymentFailedNotification',
'LogPaymentFailure',
],
},
// Event subscribers (classes that listen to multiple events)
subscribe: [
'UserEventSubscriber',
'PaymentEventSubscriber',
],
}
Event Subscribers
// app/Subscribers/UserEventSubscriber.ts
import { Subscriber } from '@stacksjs/events'
export default class UserEventSubscriber extends Subscriber {
/**
* Register the listeners for the subscriber.
*/
subscribe(events: EventDispatcher): void {
events.listen(UserRegistered, this.onUserRegistered)
events.listen(UserLoggedIn, this.onUserLoggedIn)
events.listen(UserDeleted, this.onUserDeleted)
}
async onUserRegistered(event: UserRegistered): Promise<void> {
await analytics.track('user_registered', event.user)
}
async onUserLoggedIn(event: UserLoggedIn): Promise<void> {
await event.user.update({ lastLoginAt: new Date() })
}
async onUserDeleted(event: UserDeleted): Promise<void> {
await cleanupUserData(event.userId)
}
}
Dispatching Events
import { dispatch, event } from '@stacksjs/events'
import { UserRegistered } from '@/Events/UserRegistered'
// Dispatch an event
await dispatch(new UserRegistered(user))
// Dispatch multiple events
await dispatch([
new UserRegistered(user),
new WelcomeEmailQueued(user),
])
// Dispatch without waiting (fire and forget)
dispatch(new UserRegistered(user)).catch(log.error)
// Conditional dispatch
if (user.isNewCustomer) {
await dispatch(new NewCustomerRegistered(user))
}
// Dispatch from model events
class User extends Model {
protected dispatchesEvents = {
created: UserRegistered,
deleted: UserDeleted,
}
}
Model Events
// Built-in model events
class Post extends Model {
// Lifecycle hooks
protected static booted(): void {
// Before events (can prevent action by returning false)
this.creating((post) => {
post.slug = slugify(post.title)
})
this.updating((post) => {
if (post.isDirty('title')) {
post.slug = slugify(post.title)
}
})
this.deleting((post) => {
if (post.comments.count() > 0) {
return false // Prevent deletion
}
})
// After events
this.created((post) => {
cache.tags(['posts']).flush()
})
this.saved((post) => {
SearchIndex.update(post)
})
}
}
Service Providers
Service providers are the central place of application bootstrapping, registering services, bindings, and event listeners.
Creating a Service Provider
// app/Providers/AppServiceProvider.ts
import { ServiceProvider } from '@stacksjs/support'
export default class AppServiceProvider extends ServiceProvider {
/**
* Register any application services.
*/
register(): void {
// Bind interfaces to implementations
this.app.bind('PaymentGateway', () => {
return new StripeGateway(config.services.stripe)
})
// Singleton binding (same instance every time)
this.app.singleton('Analytics', () => {
return new AnalyticsService(config.services.analytics)
})
// Contextual binding
this.app.when(PhotoController)
.needs('Storage')
.give(() => new S3Storage())
this.app.when(VideoController)
.needs('Storage')
.give(() => new CloudflareStorage())
}
/**
* Bootstrap any application services.
*/
boot(): void {
// Run after all providers are registered
// Share data with all views
View.share('appName', config.app.name)
// Register custom validation rules
Validator.extend('phone', (value) => {
return /^\+?[\d\s-]+$/.test(value)
})
// Register macros
Response.macro('success', function (data: any) {
return Response.json({ success: true, data })
})
}
}
Registering Providers
// config/app.ts
export default {
providers: [
// Framework providers
'@stacksjs/routing/RouteServiceProvider',
'@stacksjs/database/DatabaseServiceProvider',
'@stacksjs/cache/CacheServiceProvider',
'@stacksjs/queue/QueueServiceProvider',
// Application providers
'AppServiceProvider',
'AuthServiceProvider',
'EventServiceProvider',
'RouteServiceProvider',
// Third-party providers
'StripeServiceProvider',
'AnalyticsServiceProvider',
],
// Deferred providers (loaded only when needed)
deferred: [
'MailServiceProvider',
'NotificationServiceProvider',
],
}
Deferred Providers
// app/Providers/ReportServiceProvider.ts
export default class ReportServiceProvider extends ServiceProvider {
// Only load when these services are requested
provides = ['ReportGenerator', 'PdfExporter', 'CsvExporter']
// Indicates this provider is deferred
deferred = true
register(): void {
this.app.singleton('ReportGenerator', () => {
return new ReportGenerator()
})
this.app.bind('PdfExporter', PdfExporter)
this.app.bind('CsvExporter', CsvExporter)
}
}
Dependency Injection
Stacks provides automatic dependency injection through constructor injection and method injection.
Constructor Injection
// app/Controllers/UserController.ts
import { UserService } from '@/Services/UserService'
import { Logger } from '@stacksjs/clarity'
export default class UserController {
constructor(
private userService: UserService,
private logger: Logger,
) {}
async index(): Promise<Response> {
const users = await this.userService.all()
this.logger.info('Users retrieved', { count: users.length })
return Response.json(users)
}
}
Method Injection
export default class OrderController {
async store(
request: Request,
orderService: OrderService, // Injected automatically
paymentGateway: PaymentGateway, // Injected automatically
): Promise<Response> {
const order = await orderService.create(request.validated())
await paymentGateway.charge(order.total)
return Response.json(order, 201)
}
}
Interface Binding
// Define interface
interface PaymentGateway {
charge(amount: number): Promise<PaymentResult>
refund(transactionId: string): Promise<RefundResult>
}
// Implement interface
class StripeGateway implements PaymentGateway {
async charge(amount: number): Promise<PaymentResult> {
// Stripe implementation
}
async refund(transactionId: string): Promise<RefundResult> {
// Stripe implementation
}
}
// Bind in service provider
this.app.bind<PaymentGateway>('PaymentGateway', StripeGateway)
// Inject by interface
class CheckoutController {
constructor(private payment: PaymentGateway) {}
}
Container Resolution
import { app, resolve } from '@stacksjs/support'
// Resolve from container
const service = app.make<UserService>('UserService')
const payment = resolve<PaymentGateway>('PaymentGateway')
// Check if bound
if (app.bound('CustomService')) {
const custom = app.make('CustomService')
}
// Resolve with parameters
const report = app.make('ReportGenerator', {
startDate: new Date('2024-01-01'),
endDate: new Date('2024-12-31'),
})
Tagged Bindings
// In service provider
this.app.bind('DiskStorage', S3Storage)
this.app.bind('CdnStorage', CloudflareStorage)
this.app.bind('BackupStorage', GlacierStorage)
// Tag related bindings
this.app.tag(['DiskStorage', 'CdnStorage', 'BackupStorage'], 'storages')
// Resolve all tagged bindings
const storages = this.app.tagged<Storage[]>('storages')
for (const storage of storages) {
await storage.healthCheck()
}
Extending Bindings
// Extend existing binding
this.app.extend('Cache', (cache, app) => {
return new MonitoredCache(cache, app.make('Monitor'))
})
// Decorating services
this.app.extend('Logger', (logger) => {
return new LoggerWithContext(logger, {
app: config.app.name,
env: config.app.env,
})
})