This sample shows how to run a full Node-Boot application with dependency injection, configuration, persistence, scheduling, and HTTP clients without opening any HTTP port or registering any routes. It boots with NodeBoot.run(GhostServer), using @nodeboot/ghost-server as a no-HTTP adapter.
That makes it a useful reference for:
- background workers
- CLI-style applications
- auto-configuration checks
- integration tests that need the full application context without a web server
Unlike the Express, Fastify, Koa, or native HTTP samples, this one has no src/controllers/ folder at all.
- Booting a Node-Boot app with
@nodeboot/ghost-server - Using DI with
@EnableDI(Container)and@EnableComponentScan() - Using TypeORM repositories, migrations, subscribers, and transactions via
@nodeboot/starter-persistence - Running cron-based scheduled tasks via
@nodeboot/starter-scheduler - Defining outbound HTTP clients with
@nodeboot/starter-http - Binding configuration from
app-config.yamlwith@ConfigurationProperties - Starting successfully even though no HTTP server is listening
This sample does not include controllers, OpenAPI, authorization, actuator, or validation starters.
- Node.js
- pnpm
- From the monorepo root, installed workspace dependencies (
pnpm install)
No external database is required by default. The sample is configured for better-sqlite3 and creates a local SQLite database file named express-sample.db in this sample directory.
From the repository root:
pnpm install
pnpm --filter @nodeboot/express-ghost-server devOr from samples/sample-ghost-server:
pnpm devsrc/server.tscreatesnew GhostApp()and callsapp.start().GhostApp.start()returnsNodeBoot.run(GhostServer).- The app context boots normally: DI, configuration, repositories, migrations, subscribers, schedulers, and HTTP clients are registered.
- No network port is opened. Even though
app-config.yamlcontainsapp.port: 3000,GhostServerdoes not bind that port. - The process stays alive so scheduled jobs can continue running.
Default application settings in app-config.yaml:
app:
name: ghost-service
platform: tech-insights
environment: development
defaultErrorHandler: false
port: 3000
persistence:
type: better-sqlite3
synchronize: false
cache: true
migrationsRun: true
better-sqlite3:
database: express-sample.dbsrc/
├── app.ts
├── server.ts
├── clients/
├── config/
├── models/
├── persistence/
└── services/
Notably absent: src/controllers/.
That is intentional. This sample is about using Node-Boot as an application runtime for non-HTTP workloads.
Key files and folders:
src/app.tsenables:@EnableDI(Container)@EnableRepositories()@EnableScheduling()@EnableHttpClients()@EnableComponentScan()@NodeBootApplication()
src/server.tsstarts the app and logs when startup completes.src/config/AppConfigProperties.tsbinds theappsection fromapp-config.yaml.src/config/ClassTransformConfiguration.tsdisables class transformer support by default and sets both transform strategies toexposeAll.src/config/MultipleConfigurations.tsgroupsClassTransformConfigurationand the custom persistence naming strategy.src/models/containsCreateUserDtoandUpdateUserDto, which useclass-validatordecorators even though there is no HTTP validation pipeline in this sample.
src/services/schedulers.component.ts is the clearest proof that the app is doing useful work without HTTP.
It registers three cron-based tasks:
fastTask()→*/1 * * * *→ every minutecleanUp()→*/5 * * * *→ every five minutesmorningRoutine()→0 9 * * *→ every day at 9:00 AM
Each job logs a message through the injected Winston logger. Since there are no controllers or routes to hit, these scheduler logs are the most visible runtime activity after startup.
This sample uses the same persistence stack you would use in an HTTP app, just without an HTTP layer.
src/persistence/entities/User.tsUserhasid,email,password, and optionalname
src/persistence/repositories/UserRepository.ts- extends TypeORM
Repository<User> - adds
findByQueryIn()using a query builder andWHERE user.id IN (:...ids)
- extends TypeORM
src/persistence/repositories/PagingUserRepository.ts- extends
PagingAndSortingRepository<User>
- extends
1701774002463-migration.tscreates tablenb-user1701786331338-migration.tsadds thenamecolumnCustomNamingStrategyprefixes generated table names withnb-DatasourceOverridesConfigurationsets:type: better-sqlite3database: express-sample.dbsynchronize: falsemigrationsRun: true
GlobalEntityEventListener- logs load/insert/update/remove/soft-remove/recover events
- logs transaction lifecycle events, including commit and rollback
UserEntityEventListener- listens only to
User - logs before/after inserts
- calls
GreetingService.sayHello()after a user is inserted
- listens only to
src/persistence/users.init.tsdefines four initial users.UserServiceseeds those users when the repository is empty.UserService.createUser(),updateUser(), anddeleteUser()are marked@Transactional().createUser()registers a transaction commit hook.deleteUser()registers a rollback hook and then throws deliberately after deletion, demonstrating rollback behavior.
src/clients/MicroserviceHttpClient.ts shows that outbound HTTP support still works in a Ghost server application.
It is declared with:
baseURL: https://jsonplaceholder.typicode.comtimeout: 5000httpLogging: true
UserService.findExternalUsers() uses that client to call GET /users, then logs how many users were returned.
There is no controller exposing this method. In this sample, the client primarily demonstrates that Node-Boot can wire HTTP clients into services even when the application itself is not an HTTP server.
Because this sample has no controllers, the main way to exercise business logic is to resolve services from the started application context and call them directly.
@nodeboot/ghost-server also exposes getDriver() and executeAction(...), as documented in ../../servers/ghost-server/README.md. That API is useful in tests or CLI code when you want to manually execute controller-style actions without a real HTTP server.
For this sample specifically:
- use the running app context for service-level testing (
UserService, schedulers, repositories, subscribers) - use
GhostServer's driver API when you later add controller actions and still want no-HTTP execution
pnpm dev— run the sample in development mode withnodemonpnpm start— clean, build, then rundist/server.jspnpm start:prod— build, then run withNODE_ENV=productionpnpm build— compile TypeScript withtsc -p tsconfig.build.jsonpnpm postbuild— run Node-Boot AOT generationpnpm clean:build— removedist/pnpm lint/pnpm lint:fix— lint the projectpnpm format/pnpm format:fix— check or rewrite formattingpnpm test/pnpm test:coverage— run testspnpm tsc— run TypeScript directlypnpm rebuild:sqlite— rebuildbetter-sqlite3pnpm create:migration— create a new TypeORM migration undersrc/persistence/migrations/pnpm nodeboot:update— update@nodeboot/*packages
MIT