Features
Composable, reusable application modules
Features are self-contained modules that bundle services, controllers, and lifecycle hooks into reusable packages. They declare what they need (.requires()) and what they provide (.provides()), enabling modular composition with compile-time dependency checking.
What is a Feature?
A feature is created with createFeatureBuilder() — a fluent builder that produces a feature token you can .add() to your app. Features compose naturally:
import { createFeatureBuilder } from '@justscale/core';
import { LoggerService } from './logger-service';
// Simple feature — provides a service
export const LoggingFeature = createFeatureBuilder()
.name('logging')
.provides((b) => b.add(LoggerService));
// Use in app
import JustScale from '@justscale/core';
const app = JustScale()
.add(LoggingFeature)
.build();Creating a Feature
Use createFeatureBuilder() to define a feature. The .provides() callback receives a builder where you .add() services and controllers:
import { createFeatureBuilder } from '@justscale/core';
import { UserService, AuthService } from './services';
import { UsersController, AuthController } from './controllers';
export const UserFeature = createFeatureBuilder()
.name('users')
.provides((b) => b
.add(UserService)
.add(AuthService)
.add(UsersController)
.add(AuthController)
);
// Use it
import JustScale from '@justscale/core';
const app = JustScale()
.add(UserFeature)
.build();Feature Dependencies
Features can depend on tokens or other features using .requires(). Dependencies must be provided before the feature is added:
import { createFeatureBuilder, bindService } from '@justscale/core';
import { ModelRepository } from '@justscale/core/models';
import { User } from './models';
// Feature that requires a User repository and an email sender
export const AuthFeature = createFeatureBuilder()
.name('auth')
.requires(ModelRepository.of(User))
.requires(AbstractEmailSender)
.provides((b) => b
.add(PasswordService)
.add(UserService)
.add(SessionService)
.add(AuthController)
);
// When adding AuthFeature, its requirements must already be met
import JustScale, { bindRepository } from '@justscale/core';
const app = JustScale()
.add(PgClient)
.add(bindRepository(ModelRepository.of(User), UserRepository))
.add(bindService(AbstractEmailSender, ConsoleEmailSender))
.add(AuthFeature) // Requirements satisfied above
.build();Lifecycle Hooks
Features can run code when the app starts or stops using .onStart() and .onStop():
import { createFeatureBuilder } from '@justscale/core';
export const DatabaseFeature = createFeatureBuilder()
.name('database')
.onStart(async ({ resolve }) => {
const client = resolve(PgClient);
await client.connect();
console.log('Database connected');
})
.onStop(async () => {
console.log('Database disconnected');
})
.provides((b) => b.add(PgClient));Requiring Other Features
When a feature requires another feature, the required feature's provided tokens become available in the .provides() builder:
import { createFeatureBuilder } from '@justscale/core';
// Base feature
export const DatabaseFeature = createFeatureBuilder()
.name('database')
.provides((b) => b.add(PgClient));
// Feature that requires DatabaseFeature
export const UserFeature = createFeatureBuilder()
.name('users')
.requires(DatabaseFeature) // PgClient is now available
.provides((b) => b
.add(UserRepository) // Can depend on PgClient
.add(UserService)
);
// In the app, add both
import JustScale from '@justscale/core';
const app = JustScale()
.add(DatabaseFeature)
.add(UserFeature) // Works because DatabaseFeature is already added
.build();Composing Features
Build complex applications by layering features. Each feature focuses on one capability:
import JustScale, { bindService, bindRepository } from '@justscale/core';
import { ModelRepository } from '@justscale/core/models';
import { AuthFeature, AuthEndpointsFeature, User, Session } from '@justscale/auth';
const app = JustScale()
// Infrastructure
.add(PgClient)
.add(PostgresLockFeature)
.add(InMemoryProcessFeature)
// Auth (requirements: User repo, Session repo, email sender)
.add(bindRepository(ModelRepository.of(User), UserRepository))
.add(bindRepository(ModelRepository.of(Session), SessionRepository))
.add(bindService(AbstractEmailSender, ConsoleEmailSender))
.add(AuthFeature)
.add(AuthEndpointsFeature)
// Domain
.add(TicketRepository)
.add(TicketService)
.add(TicketController)
.build();Service Bindings in Features
Features can use bindService and bindRepository inside their .provides() callback to wire abstract tokens to implementations:
import { createFeatureBuilder, bindService } from '@justscale/core';
// Abstract service
abstract class AbstractLogger {
abstract log(message: string): void;
}
// Concrete implementation
class ConsoleLogger extends defineService({
inject: {},
factory: () => ({
log: (message: string) => console.log(message),
}),
}) {}
// Feature binds abstract to concrete
export const LoggingFeature = createFeatureBuilder()
.name('logging')
.provides((b) => b
.add(ConsoleLogger)
.add(bindService(AbstractLogger, ConsoleLogger))
);
// Other services can now inject AbstractLogger
class UserService extends defineService({
inject: { logger: AbstractLogger },
factory: ({ logger }) => ({
create: (user) => {
logger.log('Creating user');
return user;
},
}),
}) {}Official Features
JustScale provides official features for common functionality:
@justscale/auth— Authentication, sessions, 2FA, password reset@justscale/feature-otel— OpenTelemetry tracing and metrics@justscale/feature-shell— Interactive CLI shell for debugging
import JustScale, { bindRepository, bindService } from '@justscale/core';
import { ModelRepository } from '@justscale/core/models';
import {
AuthFeature, AuthEndpointsFeature,
User, Session, AbstractEmailSender, ConsoleEmailSender,
} from '@justscale/auth';
const app = JustScale()
.add(PgClient)
.add(bindRepository(ModelRepository.of(User), PgUserRepository))
.add(bindRepository(ModelRepository.of(Session), PgSessionRepository))
.add(bindService(AbstractEmailSender, ConsoleEmailSender))
.add(AuthFeature)
.add(AuthEndpointsFeature)
.build();Best Practices
- One feature per domain — group related functionality together
- Explicit dependencies — use
.requires()to declare what a feature needs, so TypeScript catches missing dependencies at compile time - Separate services from endpoints — provide services in one feature and controllers in another (like
AuthFeature+AuthEndpointsFeature) so consumers can build their own endpoints - Use lifecycle hooks —
.onStart()for initialization,.onStop()for cleanup - Keep features focused — a feature should represent one clear capability