Generate
Scaffold ExpressoTS v4 resources from your terminal. The CLI emits the right file, places it in the folder defined by scaffoldSchematics, and (for service) automatically wires the new module into the DI container.
expressots generate <schematic> [path] [method] [options]
# or the short alias
expressots g <schematic-alias> [path] [method] [options]
Schematics
| Schematic | Alias | Emits | v4 |
|---|---|---|---|
service | s | Controller + use case + DTO + module wiring, a full vertical slice for one HTTP route. | |
controller | c | One *.controller.ts | |
usecase | u | One *.usecase.ts | |
dto | d | One *.dto.ts | |
provider | p | One *.provider.ts | |
entity | e | One *.entity.ts | |
middleware | mi | One *.middleware.ts (ExpressoMiddleware class) | |
module | mo | One *.module.ts | |
interceptor | i | One *.interceptor.ts (@Interceptor + IInterceptor) | ✅ |
event | ev | One *.event.ts (type-safe event class) | ✅ |
handler | h | One *.handler.ts (@OnEvent(...) handler). Pair with --event | ✅ |
guard | gu | One *.guard.ts (Express canActivate guard) | ✅ |
config | cfg | One *.config.ts (defineConfig + Env + loadEnvSync + bootstrap.envFileConfig) | ✅ |
Options
| Flag | Applies to | Default | Description |
|---|---|---|---|
path | every schematic | (none) | The resource path. Two forms: folder/name or shorthand folder-name. See Path forms. |
method | controller, service | get | HTTP method: get / post / put / patch / delete. Drives the controller template body. |
--event | handler | MyEvent | The event class name the handler subscribes to. |
--priority | interceptor, handler | 10 | Numeric priority. Lower runs earlier. |
--method -m | controller, service | (none) | Shorthand for the method positional, e.g. -m post. |
Path forms
The path argument is the positional path (the option is also named -d). There is no --module= flag. The resource's module is derived from the path.
Folder / subfolder / resource
Slashes drive a one-to-one folder structure:
expressots g c users/create
src/useCases/users/
└── create.controller.ts
Shorthand
Dashes collapse into a folder/subfolder/resource directory, with the resource file named after the full kebab-case path:
expressots g c users-create
src/useCases/users/create/
└── users-create.controller.ts
Trailing slash
Forces the resource into a folder of the same name as the leaf:
expressots g c users/create/
src/useCases/users/create/
└ ── create.controller.ts
The folder form is great for explicit organization. The shorthand is great for opinionated projects where every resource ends up in its own folder. The trailing slash is the middle ground. Use it when you want a leaf folder without renaming the file.
Opinionated vs non-opinionated
The CLI behaviour depends on expressots.config.ts → opinionated:
opinionated | Behaviour |
|---|---|
true | Every schematic goes under its scaffoldSchematics folder (e.g. controllers under useCases/). Generating a service also patches the matching *.module.ts and the AppContainer.modules list. Custom folder names are auto-added to tsconfig.json paths and tsconfig.build.json. |
false | Everything goes flat under sourceRoot. The scaffoldSchematics map is only used to override the file's middle segment (the part between the kebab name and .ts). No module wiring. |
Default opinionated folders
| Schematic | Folder under sourceRoot |
|---|---|
usecase, controller, dto, service, module | useCases |
provider | providers |
entity | entities |
middleware | middleware |
interceptor | interceptors |
event, handler | events |
guard | guards |
config | config |
Override any of them in expressots.config.ts → scaffoldSchematics:
scaffoldSchematics: {
controller: "modules", // instead of "useCases"
interceptor: "aspects", // instead of "interceptors"
}
When the folder name is non-default in an opinionated project, the CLI adds a TS path alias to your tsconfigs so imports like import { MyInterceptor } from "@aspects/my.interceptor" work.
Examples
Service slice (controller + usecase + dto + module)
# Opinionated, kebab-case
expressots g s users-create post
src/useCases/users/create/
├── users-create.controller.ts # @controller("/users") + @Post("/")
├── users-create.usecase.ts # @provide UsersCreateUseCase
├── users-create.dto.ts # body schema
└── users-create.module.ts # auto-patched / created
# Also patches:
src/useCases/users/users.module.ts # adds the controller
src/app.ts # CreateModule list
Controller for a single GET
expressots g c health
src/useCases/
└── health.controller.ts
Interceptor
expressots g i Logging --priority 5
src/interceptors/
└── logging.interceptor.ts
import { Interceptor, IInterceptor, ExecutionContext, CallHandler } from "@expressots/core";
@Interceptor({ priority: 5 })
export class LoggingInterceptor implements IInterceptor {
intercept(context: ExecutionContext, next: CallHandler): unknown {
return next.handle();
}
}
See Interceptors for composition (whenInterceptor, pipeInterceptors, etc.).
Event class + handler
expressots g ev UserCreated
expressots g h EmailWelcome --event UserCreated --priority 20
src/events/
├── user-created.event.ts
└── email-welcome.handler.ts
export class UserCreatedEvent {
constructor(public readonly userId: string, public readonly email: string) {}
}
import { provide, OnEvent, IEventHandler } from "@expressots/core";
import { UserCreatedEvent } from "@events/user-created.event";
@provide(EmailWelcomeHandler)
@OnEvent(UserCreatedEvent, { priority: 20 })
export class EmailWelcomeHandler implements IEventHandler<UserCreatedEvent> {
handle(event: UserCreatedEvent) {
// Full type inference on event.userId / event.email
}
}
See Events for @When, async handlers, replay, and flow visualization.
Guard
expressots g gu Admin
src/guards/
└── admin.guard.ts
See Guards and Authorization.
Typed configuration
expressots g cfg app
src/config/
└── app.config.ts
import { defineConfig, Env, loadEnvSync } from "@expressots/core";
export const appConfig = defineConfig({
app: {
port: Env.port("PORT").default(3000),
name: Env.string("APP_NAME").default("expressots-app"),
},
database: {
url: Env.url("DATABASE_URL").required(),
},
});
export type AppConfig = typeof appConfig;
Wire it into bootstrap:
import { bootstrap, loadEnvSync } from "@expressots/core";
import { App } from "./app";
loadEnvSync({ files: { development: ".env", production: ".env.prod" } });
await bootstrap(App, { port: 3000 });
See Configuration for the full Env.* surface.
Overwrite confirmation
If the target file already exists, the CLI asks for confirmation interactively (default: yes). In CI, pipe yes or pre-delete the file:
yes | expressots g c users/create
Constraints
| Constraint | Why |
|---|---|
expressots.config.ts must exist | The CLI walks up from cwd; if no config is found it exits with No config file found!. |
sourceRoot must be non-empty | The CLI refuses to scaffold into an empty source root. |
Nested path depth ≤ 4 for service / controller | Prevents accidental deep trees; the CLI errors with a clear message past four levels. |
Don't combine path and a leading slash | All paths are relative to sourceRoot. |
See also
- Configuration File:
expressots.config.tsschema. - Interceptors, Events, Guards, Authorization, Configuration: the runtime features the new schematics target.
new: create the project scaffold this command writes into.