fragment-ts
v4.0.2
Published
Spring Boot-style framework for TypeScript with Express and TypeORM
Readme
Fragment TS
Fragment TS is a Spring Boot-inspired TypeScript framework for Express and TypeORM, built around dependency injection and decorators, with a fully modular CLI.
This README is intentionally long and comprehensive so it can serve as a landing page for the framework site.
Table of Contents
- Overview
- Quick Start
- Philosophy
- Project Structure
- Bootstrapping
- Dependency Injection
- HTTP Pipeline
- TypeORM
- Plugins
- Notifications
- Jobs and Cron
- Documentation Generator
- CLI Overview
- Configuration
- Testing
- Logging
- Troubleshooting
- FAQ
- Glossary
Overview
Fragment TS is designed for teams who want consistency, explicit structure, and fast iteration in TypeScript. The framework centers on class decorators that describe behavior and a DI container that resolves dependencies.
Key ideas:
- Everything is a class with metadata.
- No hidden magic: configuration is in
fragment.json. - One unified CLI that can be called programmatically.
- Features are optional but designed to work together.
Quick Start
- Install the CLI:
npm install -g fragment-ts - Initialize:
fragment init my-app - Move into the directory:
cd my-app - Run the dev server:
fragment serve
Hello World example:
import {
FragmentApplication,
FragmentWebApplication,
Controller,
Get,
} from "fragment-ts";
@Controller("/")
class HelloController {
@Get()
index() {
return { message: "Hello Fragment" };
}
}
@FragmentApplication({ port: 3000 })
class App {}
async function bootstrap() {
const app = new FragmentWebApplication();
await app.bootstrap(App);
}
bootstrap();Philosophy
Fragment TS borrows the clarity of Spring Boot and the ergonomics of modern TypeScript. Your application should read like an architecture diagram. Your configuration should be explicit and stable. Your framework should be a partner, not a black box.
Project Structure
Structure is controlled by fragment.json:
layered:src/controllers,src/services,src/repositoriesmodule:src/modules/<name>/...feature:src/features/<name>/...
The CLI generators honor the structure you choose.
Bootstrapping
import { FragmentApplication, FragmentWebApplication } from "fragment-ts";
@FragmentApplication({ port: 3000, autoScan: true })
class Application {}
async function bootstrap() {
const app = new FragmentWebApplication();
await app.bootstrap(Application);
}
bootstrap();Dependency Injection
Constructor injection is automatic, property injection is opt-in.
import { Service, Autowired, Value } from "fragment-ts";
@Service()
class MailService {
@Value("${MAIL_FROM:[email protected]}")
private from!: string;
}
@Service()
class UserService {
@Autowired()
private mailer!: MailService;
@Autowired()
private secondaryMailer!: MailService;
}HTTP Pipeline
Fragment wraps Express and adds middleware, guards, interceptors, and exception filters.
import { Controller, Get, FragmentGuard } from "fragment-ts";
class AuthGuard {
canActivate(req: any) {
return !!req.user;
}
}
@FragmentGuard(AuthGuard)
@Controller("/secure")
class SecureController {
@Get()
profile() {
return { ok: true };
}
}TypeORM
TypeORM is wired through TypeORMModule and fragment.json.
Fragment supports:
- a legacy default connection through
database - multiple named connections through
databases - per-entity database assignment with
@UseDatabase("name") - repository injection through
@InjectRepository(...) - runtime switching through
repo.db("name")orrepo.db(dbConfig)
Simple single-database setup:
{
"database": {
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "app",
"password": "secret",
"database": "app_main",
"entities": ["src/**/*.entity.ts"],
"migrations": ["src/migrations/**/*.ts"]
}
}Named multi-database setup:
{
"databases": {
"primary": {
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "app",
"password": "secret",
"database": "app_main",
"entities": ["src/**/*.entity.ts"],
"migrations": ["src/migrations/primary/**/*.ts"]
},
"analytics": {
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "app",
"password": "secret",
"database": "app_analytics",
"entities": ["src/**/*.entity.ts"],
"migrations": ["src/migrations/analytics/**/*.ts"]
}
}
}Typical usage:
import {
Column,
Entity,
InjectRepository,
PrimaryGeneratedColumn,
Repository,
Service,
UseDatabase,
type DbConfig,
} from "fragment-ts";
@UseDatabase("analytics")
@Entity()
class EventRecord {
@PrimaryGeneratedColumn()
id!: number;
@Column()
event!: string;
}
@Service()
class EventService {
@InjectRepository(EventRecord)
private events!: Repository<EventRecord>;
async fromAssignedDatabase() {
return this.events.findAll();
}
async fromNamedDatabase() {
return this.events.db("analytics").findAll();
}
async fromRuntimeConfig(dbConfig: DbConfig) {
return this.events.db(dbConfig).findAll();
}
}Rules of thumb:
- if
databaseexists, it remains the default connection - if only one named database exists in
databases, Fragment treats it as the primary runtime default - if multiple named databases exist and there is no default, entity/repository usage should be explicit
- migration and seed commands prompt for the database target when more than one database exists
Plugins
Plugins are first-class and can be installed with fragment add <package>.
Use qualifiers to disambiguate multiple plugin instances.
Mailers can be registered via @Mailer or via config-driven providers in fragment.json.
Templates can be registered with @MailTemplate.
Notifications
Notifications resolve channels through @NotificationChannel metadata.
Default channels are configured in fragment.json.
Jobs and Cron
Jobs can be defined with @Job() and scheduled with @Cron() or the fluent scheduler.
Documentation Generator
Use @ApiTag, @ApiOperation, @ApiResponse, and @ApiProperty to generate OpenAPI-style docs.
Runtime docs are served at /docs and /docs.json.
Per-module runtime docs can be served at /docs/modules/:module.
CLI Overview
The CLI (fragment or frg) provides scaffolding, diagnostics, and automation.
Selected commands:
- fragment init [dir]
- fragment init --type=plugin
- fragment serve
- fragment build
- fragment test
- fragment lint
- fragment route:list
- fragment config:cache
- fragment cache:clear
- fragment doc:generate
- fragment plugin:list
- fragment plugin:info
- fragment plugin:remove
- fragment plugin:enable
- fragment plugin:disable
- fragment add
- fragment make:controller
- fragment make:service
- fragment make:client
- fragment make:resource
- fragment make:entity
- fragment make:dto
- fragment make:repository
- fragment make:middleware
- fragment make:guard
- fragment make:interceptor
- fragment make:filter
- fragment make:feature
- fragment make:job
- fragment make:mail
- fragment make:notification
Configuration
Fragment is configured via fragment.json.
Example:
{
"structure": { "type": "module" },
"verbose": false,
"logging": { "level": "info", "structured": false },
"docs": {
"path": "/docs",
"enabled": true,
"perModule": true,
"perModuleRoutes": true
},
"database": {
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "postgres",
"password": "password",
"database": "fragment",
"entities": ["src/**/*.entity.ts"],
"migrations": ["src/migrations/**/*.ts"]
},
"mail": {
"default": "primary",
"mailers": {
"primary": { "driver": "console", "from": "[email protected]" }
}
},
"notifications": { "defaultChannels": ["mail"] },
"jobs": { "enabled": true, "timezone": "UTC" },
"plugins": ["my-fragment-plugin"],
"pluginAutoQualify": true
}Configuration keys (high-level):
- structure.type
- verbose
- logging.level
- logging.structured
- database.*
- database.entities
- database.migrations
- databases
- databases..*
- databases..entities
- databases..migrations
- mail.default
- mail.mailers
- mail.templatesDir
- mail.queue.enabled
- notifications.defaultChannels
- jobs.enabled
- jobs.timezone
- docs.enabled
- docs.path
- docs.outputDir
- docs.template
- docs.perModule
- docs.perModuleRoutes
- plugins
- pluginAutoQualify
Testing
Fragment includes a lightweight test runner:
import { describe, it, expect, initTestRunner } from "fragment-ts";
initTestRunner();
describe("math", () => {
it("adds", () => {
expect(1 + 1).toBe(2);
});
});Logging
Use FragmentLogger with configurable levels and structured output.
verbose or logging.level controls the output verbosity.
Feature Index
Core decorators:
- @FragmentApplication(options)
- @Controller(path?)
- @Service()
- @FragmentRepository()
- @Injectable(scope?)
- @AutoConfiguration()
Injection decorators:
- @Autowired()
- @Inject(token)
- @InjectRepository(entity)
- @Value(expression)
- @Qualifier(name)
- @Optional()
- @Lazy()
HTTP decorators:
- @Get(path?)
- @Post(path?)
- @Put(path?)
- @Delete(path?)
- @Patch(path?)
- @Body()
- @Param(name?)
- @Query(name?)
- @Header(name?)
- @Req()
- @Res()
Pipeline decorators:
- @FragmentMiddleware(MiddlewareClass, { order? })
- @FragmentGuard(GuardClass)
- @FragmentInterceptor(InterceptorClass)
- @FragmentExceptionFilter(ExceptionFilterClass)
Lifecycle decorators:
- @PostConstruct()
- @PreDestroy()
- @OnStartup(order?)
- @OnShutdown(order?)
Plugin decorators:
- @Plugin(options)
- @Plugin(token, qualifier?) (property)
- @OnPluginInit(order?)
- @OnPluginShutdown(order?)
Mail decorators:
- @Mailer(options)
- @Mail(qualifier?) (property)
- @MailTemplate(options)
Notification decorators:
- @NotificationChannel(options)
Jobs decorators:
- @Job(options)
- @Cron(expression, options?)
Docs decorators:
- @ApiTag(tag)
- @ApiOperation({ summary, description })
- @ApiResponse({ status, description, type, isArray, example })
- @ApiProperty({ description, required, example, type })
Troubleshooting
- If decorators are not executed, ensure
experimentalDecoratorsandemitDecoratorMetadataare true. - If no components are discovered, check scan patterns and file suffixes.
- If MobX is missing, install it and rerun the build.
- If docs output is empty, confirm controllers are registered and
@Api*metadata exists.
FAQ
Q: Does Fragment require TypeORM? A: No, but TypeORM integration is first-class.
Q: Can I run commands programmatically?
A: Yes. Use CommandRunner.run().
Q: How do I disable docs routes?
A: Set docs.enabled to false in fragment.json.
Glossary
- Plugin: External extension with lifecycle hooks.
- Module: A grouped feature in module structure.
Best Practices
- Use
@FragmentApplication(options)to register core application components. - Use
@Controller(path?)to register core application components. - Use
@Service()to register core application components. - Use
@FragmentRepository()to register core application components. - Use
@Injectable(scope?)to register core application components. - Use
@AutoConfiguration()to register core application components. @Autowired()clarifies injection intent and dependency scope.@Inject(token)clarifies injection intent and dependency scope.@InjectRepository(entity)clarifies injection intent and dependency scope.@Value(expression)clarifies injection intent and dependency scope.@Qualifier(name)clarifies injection intent and dependency scope.@Optional()clarifies injection intent and dependency scope.@Lazy()clarifies injection intent and dependency scope.@Get(path?)defines routing and request binding explicitly.@Post(path?)defines routing and request binding explicitly.@Put(path?)defines routing and request binding explicitly.@Delete(path?)defines routing and request binding explicitly.@Patch(path?)defines routing and request binding explicitly.@Body()defines routing and request binding explicitly.@Param(name?)defines routing and request binding explicitly.@Query(name?)defines routing and request binding explicitly.@Header(name?)defines routing and request binding explicitly.@Req()defines routing and request binding explicitly.@Res()defines routing and request binding explicitly.@FragmentMiddleware(MiddlewareClass, { order? })keeps cross-cutting concerns declarative.@FragmentGuard(GuardClass)keeps cross-cutting concerns declarative.@FragmentInterceptor(InterceptorClass)keeps cross-cutting concerns declarative.@FragmentExceptionFilter(ExceptionFilterClass)keeps cross-cutting concerns declarative.@PostConstruct()ensures deterministic lifecycle order.@PreDestroy()ensures deterministic lifecycle order.@OnStartup(order?)ensures deterministic lifecycle order.@OnShutdown(order?)ensures deterministic lifecycle order.@Plugin(options)exposes plugin lifecycle and injection points.@Plugin(token, qualifier?) (property)exposes plugin lifecycle and injection points.@OnPluginInit(order?)exposes plugin lifecycle and injection points.@OnPluginShutdown(order?)exposes plugin lifecycle and injection points.@Mailer(options)keeps mail logic modular and testable.@Mail(qualifier?) (property)keeps mail logic modular and testable.@MailTemplate(options)keeps mail logic modular and testable.@NotificationChannel(options)registers custom delivery channels.@Job(options)schedules background work consistently.@Cron(expression, options?)schedules background work consistently.@ApiTag(tag)keeps API docs close to controllers.@ApiOperation({ summary, description })keeps API docs close to controllers.@ApiResponse({ status, description, type, isArray, example })keeps API docs close to controllers.@ApiProperty({ description, required, example, type })keeps API docs close to controllers.- CLI:
fragment init [dir]is part of the primary workflow. - CLI:
fragment init --type=pluginis part of the primary workflow. - CLI:
fragment serveis part of the primary workflow. - CLI:
fragment buildis part of the primary workflow. - CLI:
fragment testis part of the primary workflow. - CLI:
fragment lintis part of the primary workflow. - CLI:
fragment route:listis part of the primary workflow. - CLI:
fragment config:cacheis part of the primary workflow. - CLI:
fragment cache:clearis part of the primary workflow. - CLI:
fragment doc:generateis part of the primary workflow. - CLI:
fragment plugin:listis part of the primary workflow. - CLI:
fragment plugin:info <name>is part of the primary workflow. - CLI:
fragment plugin:remove <name>is part of the primary workflow. - CLI:
fragment plugin:enable <name>is part of the primary workflow. - CLI:
fragment plugin:disable <name>is part of the primary workflow. - CLI:
fragment add <package>is part of the primary workflow. - CLI:
fragment make:controller <name>is part of the primary workflow. - CLI:
fragment make:service <name>is part of the primary workflow. - CLI:
fragment make:client <name>is part of the primary workflow. - CLI:
fragment make:resource <name>is part of the primary workflow. - CLI:
fragment make:entity <name>is part of the primary workflow. - CLI:
fragment make:dto <name>is part of the primary workflow. - CLI:
fragment make:repository <name>is part of the primary workflow. - CLI:
fragment make:middleware <name>is part of the primary workflow. - CLI:
fragment make:guard <name>is part of the primary workflow. - CLI:
fragment make:interceptor <name>is part of the primary workflow. - CLI:
fragment make:filter <name>is part of the primary workflow. - CLI:
fragment make:feature <name>is part of the primary workflow. - CLI:
fragment make:job <name>is part of the primary workflow. - CLI:
fragment make:mail <name>is part of the primary workflow. - CLI:
fragment make:notification <name>is part of the primary workflow. - Config:
structure.typeis a stable knob for behavior. - Config:
verboseis a stable knob for behavior. - Config:
logging.levelis a stable knob for behavior. - Config:
logging.structuredis a stable knob for behavior. - Config:
database.*is a stable knob for behavior. - Config:
database.entitiesis a stable knob for behavior. - Config:
database.migrationsis a stable knob for behavior. - Config:
mail.defaultis a stable knob for behavior. - Config:
mail.mailersis a stable knob for behavior. - Config:
mail.templatesDiris a stable knob for behavior. - Config:
mail.queue.enabledis a stable knob for behavior. - Config:
notifications.defaultChannelsis a stable knob for behavior. - Config:
jobs.enabledis a stable knob for behavior. - Config:
jobs.timezoneis a stable knob for behavior. - Config:
docs.enabledis a stable knob for behavior. - Config:
docs.pathis a stable knob for behavior. - Config:
docs.outputDiris a stable knob for behavior. - Config:
docs.templateis a stable knob for behavior. - Config:
docs.perModuleis a stable knob for behavior. - Config:
docs.perModuleRoutesis a stable knob for behavior. - Config:
pluginsis a stable knob for behavior. - Config:
pluginAutoQualifyis a stable knob for behavior.
Recent Updates
The following additions reflect the current framework behavior and extend the material above.
Static-first outbound clients
FrgClient no longer needs instance-style initialization for normal usage. The primary pattern is now class-level methods:
import { FrgClient, type FrgClientConfig } from "fragment-ts";
class CreateCustomerPayload {
fullName!: string;
email!: string;
}
class CustomerResponseDto {
id!: string;
email!: string;
}
class CustomerClient extends FrgClient {
protected static override defaults(): FrgClientConfig {
return {
baseURL: process.env.CUSTOMER_API_URL || "https://api.example.com",
timeout: 10000,
};
}
static createCustomer(payload: CreateCustomerPayload) {
return this.post<CustomerResponseDto, CreateCustomerPayload>(
"/customers",
payload,
);
}
}Multi-database setups can skip the top-level database block and use databases only:
{
"databases": {
"primary": {
"type": "postgres",
"database": "app_main",
"entities": ["src/**/*.entity.ts"]
},
"analytics": {
"type": "postgres",
"database": "app_analytics",
"entities": ["src/**/*.entity.ts"]
}
}
}If there is only one named database, Fragment treats it as the primary default at runtime. If there are multiple named databases and no default, entities should opt in explicitly with @UseDatabase("name") or repository injection should pass the database name.
Multi Database
Fragment now supports:
- one legacy default database via
database - any number of named databases via
databases - per-entity targeting with
@UseDatabase("analytics") - repository injection with
@InjectRepository(Entity, "analytics") - runtime repository switching with
repo.db("analytics") - runtime repository switching with ad hoc config objects via
repo.db(dbConfig)
Migration and seed commands are database-aware too. Commands like frg migrate, frg migrate:generate, frg seed:run, and frg seed:create now prompt for the target database before running when more than one database is configured. You can also pass --database <name> to skip the prompt.
End-to-end flow:
- Define one or more connections in
fragment.json - Assign an entity with
@UseDatabase("name"), or keep it on the default database - Inject the repository with
@InjectRepository(Entity)or@InjectRepository(Entity, "name") - Optionally switch per-query with
repo.db("name")orrepo.db(dbConfig) - Run
frg migrateorfrg seed:runand choose the database when prompted
Programmatic example:
import {
type DbConfig,
InjectRepository,
Repository,
Service,
} from "fragment-ts";
@Service()
export class EventService {
@InjectRepository(EventRecord)
private events!: Repository<EventRecord>;
async run(acmeOrgDbConfig: DbConfig) {
return this.events.db(acmeOrgDbConfig).findAll();
}
}What this adds:
- Static
get,post,put,patch,delete,request, andraw - Shared per-client axios instance
- Static request and response interceptors
FrgClientErrorandFrgClientValidationErrorFrgClientResponse<T>for raw envelope access
Validation defaults and alias-aware fields
@Validate(...) now supports defaultValue, which is applied before validation:
import { CurrencyCode, Email, RequiredString, Validate } from "fragment-ts";
class CreateUserDto {
@RequiredString(2, { name: "fullName", required: true })
name!: string;
@Email({ name: "emailAddress", required: true })
email!: string;
@CurrencyCode({ name: "currency", defaultValue: "USD" })
currency!: string;
@Validate({ name: "role", defaultValue: "user" })
role!: string;
}Multiple implementations through qualifiers
@Service(...) now supports qualifier and provides, and injection supports @InjectAll(...):
import { Inject, InjectAll, Qualifier, Service } from "fragment-ts";
abstract class PaymentGateway {}
@Service({ qualifier: "paypal", provides: [PaymentGateway] })
class PaypalGateway extends PaymentGateway {}
@Service({ qualifier: "stripe", provides: [PaymentGateway] })
class StripeGateway extends PaymentGateway {}
@Service()
class CheckoutService {
@Inject(PaymentGateway)
@Qualifier("paypal")
private gateway!: PaymentGateway;
@InjectAll(PaymentGateway)
private gateways!: PaymentGateway[];
}Simpler notifications
You can now send notifications without constructing NotificationManager directly:
import { Notifications, notify, notifyBulk, notifyMany } from "fragment-ts";
await notify(new WelcomeNotification(), user);
await notifyMany(new WelcomeNotification(), users);
await notifyBulk([
{ notification: new WelcomeNotification(), notifiable: userA },
{ notification: new WelcomeNotification(), notifiable: userB },
]);
await Notifications.sendBulk([
{ notification: new WelcomeNotification(), notifiable: userA },
{ notification: new WelcomeNotification(), notifiable: userB },
]);CLI notes
The current public CLI surface includes:
frg installfrg install:apifrg servefrg buildfrg testfrg make:client <name>
Suggested YouTube placements
For the public documentation experience, these are good places to embed walkthroughs:
- Quick start and first endpoint
- DTO validation and alias/default handling
- FrgClient patterns for upstream APIs
- Notifications and bulk delivery
- Qualifiers and
@InjectAll(...)
Runtime policy, storage, and TTL updates
Fragment now also supports:
@CanAccess(...)for declarative ABAC@Cache(...)and@InvalidateCache(...)for smart caching@RateLimit(...)with local or Redis-backed storage@PII(),@Encrypted(), and@Unmask(...)for privacy-aware responses and log redaction@Trace(...)for runtime tracing@Workflow()and@Step(...)for resumable workflows@Returns(...)andfrg sdk:generatefor SDK-friendly response contracts
Use the shared TTL enum across framework features that need durations:
import { Cache, RateLimit, TTL } from "fragment-ts";
@RateLimit({ requests: 100, window: TTL.ONE_MINUTE })
@Cache({ ttl: TTL.FIVE_MINUTES, key: "user:${params.id}" })
class ExampleController {}Storage is configurable from fragment.json:
{
"cache": {
"driver": "redis",
"redis": {
"url": "${REDIS_URL:redis://127.0.0.1:6379}"
}
},
"rateLimit": {
"driver": "redis",
"redis": {
"url": "${REDIS_URL:redis://127.0.0.1:6379}"
}
},
"workflows": {
"storage": "redis",
"redis": {
"url": "${REDIS_URL:redis://127.0.0.1:6379}"
}
},
"privacy": {
"redactLogs": true,
"mask": "[REDACTED]",
"encryptionKey": "${FRAGMENT_ENCRYPTION_KEY:dev-key}"
},
"tracing": {
"enabled": true,
"exporter": "logger"
}
}