Skip to main content
Version: 4.0.0-preview

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

SchematicAliasEmitsv4
servicesController + use case + DTO + module wiring, a full vertical slice for one HTTP route.
controllercOne *.controller.ts
usecaseuOne *.usecase.ts
dtodOne *.dto.ts
providerpOne *.provider.ts
entityeOne *.entity.ts
middlewaremiOne *.middleware.ts (ExpressoMiddleware class)
modulemoOne *.module.ts
interceptoriOne *.interceptor.ts (@Interceptor + IInterceptor)
eventevOne *.event.ts (type-safe event class)
handlerhOne *.handler.ts (@OnEvent(...) handler). Pair with --event
guardguOne *.guard.ts (Express canActivate guard)
configcfgOne *.config.ts (defineConfig + Env + loadEnvSync + bootstrap.envFileConfig)

Options

FlagApplies toDefaultDescription
pathevery schematic(none)The resource path. Two forms: folder/name or shorthand folder-name. See Path forms.
methodcontroller, servicegetHTTP method: get / post / put / patch / delete. Drives the controller template body.
--eventhandlerMyEventThe event class name the handler subscribes to.
--priorityinterceptor, handler10Numeric priority. Lower runs earlier.
--method -mcontroller, 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
Why three forms

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:

opinionatedBehaviour
trueEvery 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.
falseEverything 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

SchematicFolder under sourceRoot
usecase, controller, dto, service, moduleuseCases
providerproviders
entityentities
middlewaremiddleware
interceptorinterceptors
event, handlerevents
guardguards
configconfig

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
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
user-created.event.ts
export class UserCreatedEvent {
constructor(public readonly userId: string, public readonly email: string) {}
}
email-welcome.handler.ts
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
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:

main.ts
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

ConstraintWhy
expressots.config.ts must existThe CLI walks up from cwd; if no config is found it exits with No config file found!.
sourceRoot must be non-emptyThe CLI refuses to scaffold into an empty source root.
Nested path depth ≤ 4 for service / controllerPrevents accidental deep trees; the CLI errors with a clear message past four levels.
Don't combine path and a leading slashAll paths are relative to sourceRoot.

See also