opticore-feature-component
v1.0.3
Published
CLI to create feature component
Readme
opticore-feature-component
OptiCore Feature Component is a package that provides an interactive CLI to generate features in an OptiCoreJs project.
Three scaffolding modes are available: Simple Component, Clean Architecture step by step, and Full Clean Architecture.
Table of Contents
- Prerequisites
- Installation
- Running the CLI
- Global Interactive Flow
- Option 1 — Simple Component
- Option 2 — CLEAN Architecture by step
- Option 3 — Full CLEAN Architecture component
- Naming Rules
- Automatic Router Registration
- Contributing
Prerequisites
- Node.js ≥ 18
- TypeScript ≥ 5
- A project exposing the
src/features/directory at the root (the CLI creates features there)
my-project > src > app > router > register.router.tsThe register.router.ts file is automatically updated when a feature router is created.
If the
featuresfolder is missing when the server launches, the CLI displays an error and stops.
Installation
As a dev dependency (recommended)
npm install --save-dev opticore-feature-component
# or
yarn add -D opticore-feature-componentGlobally
npm install -g opticore-feature-componentFrom source (monorepo)
# from the package root
npm install
npm run buildRunning the CLI
With npx or npm (no global install)
npx create-feature-modulenpm exec create-feature-moduleVia a package.json script (recommended)
Add a script to your project's package.json:
{
"scripts": {
"feature": "create-feature-module"
}
}Then run:
npm run feature
# or
yarn featureGlobal installation
create-feature-moduleGlobal Interactive Flow
When launched, the CLI displays the following banner before any prompt:
██████╗ ██████╗ ████████╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗ ██╗ ███████╗
██╔═══██╗ ██╔══██╗ ╚══██╔══╝ ██║ ██╔════╝ ██╔═══██╗ ██╔══██╗ ██╔════╝ ██║ ██╔════╝
██║ ██║ ██████╔╝ ██║ ██║ ██║ ██║ ██║ ██████╔╝ █████╗ ██║ ███████╗
██║ ██║ ██╔═══╝ ██║ ██║ ██║ ██║ ██║ ██╔══██╗ ██╔══╝ ██ ██║ ╚════██║
╚██████╔╝ ██║ ██║ ██║ ╚██████╗ ╚██████╔╝ ██║ ██║ ███████╗ ╚█████╔╝ ███████║
╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚════╝ ╚══════╝
OPTICORE F E A T U R E M O D U L E
Create · Structure · Scaffold · Generate · OrganizeThen follows a 3-step flow before presenting the scaffold options:
╭────────────────────────────────────────────────────╮
│ │
│ Welcome to Feature Component CLI │
│ │
╰────────────────────────────────────────────────────╯
◆ Please choose the creation principle for your feature :
│ ● OptiCoreJs CLEAN Module (recommended)
│ ○ Custom feature
└
◆ Enter feature's name :
│ login
└
◆ Choose a type of component :
│ ○ Simple component
│ ○ CLEAN Architecture component by step
│ ● Full CLEAN Architecture component (default)
└Step 1 — Creation Principle
| Choice | Behavior |
|---|---|
| OptiCoreJs CLEAN Module | Enables automated scaffolding — continues to step 2 |
| Custom feature | Displays an information message and exits (manual creation) |
Step 2 — Feature Name
The name must follow the rule: ^[a-z][A-Za-z]+$
→ camelCase, starts with a lowercase letter, minimum 2 characters.
✅ userProfile
✅ productOrder
✅ authToken
❌ UserProfile (starts with uppercase)
❌ user_profile (underscore not allowed)
❌ user (only 1 character after the first letter)Step 3 — Component Type → see the following sections.
Press Ctrl+C at any step to cancel the operation and remove any directories already created.
Option 1 — Simple Component
Flat and pragmatic structure. Ideal for lightweight features without a domain layer.
What is generated
╔═══════════════════════════════════════════════════════════════════════╗
║ SIMPLE COMPONENT — src/features/<featureName>/ ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌───────────────────────────────────────────────────────────────┐ ║
║ │ 🌐 ROUTES — HTTP entry point │ ║
║ │ routes/<featureName>.router.ts │ ║
║ │ routes/<featureName>.router.handler.ts │ ║
║ └────────────────────────────┬──────────────────────────────────┘ ║
║ │ handles HTTP requests ║
║ ┌────────────────────────────▼──────────────────────────────────┐ ║
║ │ 🎮 CONTROLLER — request / response orchestration │ ║
║ │ controllers/<featureName>.controller.ts │ ║
║ └────────────────────────────┬──────────────────────────────────┘ ║
║ │ delegates business logic ║
║ ┌────────────────────────────▼──────────────────────────────────┐ ║
║ │ ⚙️ SERVICE — business logic │ ║
║ │ services/<featureName>.service.ts │ ║
║ └────────────────────────────┬──────────────────────────────────┘ ║
║ │ reads & writes data ║
║ ┌────────────────────────────▼──────────────────────────────────┐ ║
║ │ 🗄️ REPOSITORY — data access │ ║
║ │ repositories/<featureName>.repository.ts │ ║
║ └────────────────────────────┬──────────────────────────────────┘ ║
║ │ shapes data ║
║ ┌────────────────────────────▼──────────────────────────────────┐ ║
║ │ 📦 MODEL — data shape │ ║
║ │ models/<featureName>.model.ts │ ║
║ └───────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════╝Example — order feature
src/features/order/
├── models/
│ └── order.model.ts
├── repositories/
│ └── order.repository.ts
├── services/
│ └── order.service.ts
├── controllers/
│ └── order.controller.ts
└── routes/
├── order.router.handler.ts
└── order.router.tsThe CLI asks whether you want methods in the controller:
◆ Do you want to add methods to the controller?
│ ● Yes ○ No
└
◆ Enter the method names (comma separated):
│ create, findAll, findById, update, delete
└Generated content — order.service.ts
import { OrderRepository } from "../repositories/order.repository";
import { OrderModel } from "../models/order.model";
export class OrderService {
private readonly repository: OrderRepository;
constructor() {
this.repository = new OrderRepository();
}
async findAll(): Promise<OrderModel[]> {
return this.repository.findAll();
}
async findById(id: string): Promise<OrderModel | null> {
return this.repository.findById(id);
}
async create(data: Record<string, unknown>): Promise<OrderModel> {
const model = new OrderModel(String(Date.now()));
// TODO: Map data fields onto model
return this.repository.create(model);
}
async update(id: string, data: Record<string, unknown>): Promise<OrderModel | null> {
const existing = await this.repository.findById(id);
if (!existing) return null;
// TODO: Apply data fields onto existing model
return this.repository.update(existing);
}
async delete(id: string): Promise<boolean> {
return this.repository.delete(id);
}
}Generated content — order.controller.ts (methods create, findAll)
import { Request, Response } from "express";
import { ResponseHandler, HttpStatusCode, IResponseHandlerSuccessData } from "opticore-http-response";
import { OrderService } from "../services/order.service";
export class OrderController {
private static buildService(): OrderService {
return new OrderService();
}
static async create(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
try {
const service = OrderController.buildService();
const result = await service.create(req.body);
return ResponseHandler.success(result, "created", HttpStatusCode.CREATED);
} catch (error) {
return OrderController.handleError(error);
}
}
static async findAll(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
try {
const service = OrderController.buildService();
const results = await service.findAll();
return ResponseHandler.success(results, "success", HttpStatusCode.OK);
} catch (error) {
return OrderController.handleError(error);
}
}
private static handleError(error: unknown) {
const message = error instanceof Error ? error.message : "Internal server error";
return ResponseHandler.error(message, HttpStatusCode.INTERNAL_SERVER_ERROR);
}
}Automatic HTTP method mapping
The CLI infers the HTTP verb and path from the method name:
| Method name (examples) | Verb | Path |
|---|---|---|
| findAll, getAll | GET | /<featureName> |
| findById, getById, getOne | GET | /<featureName>/:id |
| create, add | POST | /<featureName> |
| update, edit | PUT | /<featureName>/:id |
| delete, remove | DELETE | /<featureName>/:id |
| any other name | GET | /<featureName>/<methodName> |
Option 2 — CLEAN Architecture by step
File-by-file interactive mode. The CLI proposes each component one at a time and only creates the ones you confirm. All created files are empty — no template is injected.
Step-by-step flow
── Domain ──────────────────────────────────────────────────────
◆ Entity → payment.entity.ts
│ ○ Yes ● No
└
◆ Event → payment.event.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/domain/events/payment.event.ts
◆ Exception → payment.exception.ts
│ ○ Yes ● No
└
◆ Repo Interface → payment.repository.interface.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/application/ports/repositories/payment.repository.interface.ts
◆ Presenter Port → payment.presenter.interface.ts
│ ○ Yes ● No
└
◆ Service Port → payment.service.ts
│ ○ Yes ● No
└
◆ DTO → payment.dto.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/application/dtos/payment.dto.ts
◆ Use Case → payment.usecase.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/application/use-cases/payment.usecase.ts
◆ Repo Impl → payment.repository.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/infrastructure/adapters/repositories/payment.repository.ts
◆ Presenter Impl → payment.presenter.ts
│ ○ Yes ● No
└
◆ Controller → payment.controller.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/infrastructure/adapters/controllers/payment.controller.ts
◆ Router Handler → payment.router.handler.ts
│ ○ Yes ● No
└
◆ Router → payment.router.ts
│ ○ Yes ● No
└
🎉 Feature "payment" — 5 file(s) created step by step.Result for the example above
╔══════════════════════════════════════════════════════════════════════════╗
║ CLEAN by step — src/features/payment/ (partial selection) ║
╠══════════════════════════════════════════════════════════════════════════╣
║ ║
║ ╔════════════════════════════════════════════════════════════════════╗ ║
║ ║ 🟦 APPLICATION ║ ║
║ ║ ║ ║
║ ║ dtos/ ║ ║
║ ║ └── ✅ payment.dto.ts (empty) ║ ║
║ ║ ║ ║
║ ║ ports/repositories/ ║ ║
║ ║ └── ✅ payment.repository.interface.ts (empty) ║ ║
║ ║ ║ ║
║ ║ use-cases/ ║ ║
║ ║ └── ✅ payment.usecase.ts (empty) ║ ║
║ ╚════════════════════════════════════════════════════════════════════╝ ║
║ ║
║ ╔════════════════════════════════════════════════════════════════════╗ ║
║ ║ 🟠 INFRASTRUCTURE ║ ║
║ ║ ║ ║
║ ║ adapters/controllers/ ║ ║
║ ║ └── ✅ payment.controller.ts (empty) ║ ║
║ ║ ║ ║
║ ║ adapters/repositories/ ║ ║
║ ║ └── ✅ payment.repository.ts (empty) ║ ║
║ ╚════════════════════════════════════════════════════════════════════╝ ║
╚══════════════════════════════════════════════════════════════════════════╝
⚠ Directories are only created for confirmed files.
⚠ If no file is selected, nothing is written to disk.Available components
| Group | Label | File created | Directory |
|---|---|---|---|
| Domain | Entity | <n>.entity.ts | domain/entities/ |
| Domain | Event | <n>.event.ts | domain/events/ |
| Domain | Exception | <n>.exception.ts | domain/exceptions/ |
| Application | Repo Interface | <n>.repository.interface.ts | application/ports/repositories/ |
| Application | Presenter Port | <n>.presenter.interface.ts | application/ports/presenters/ |
| Application | Service Port | <n>.service.ts | application/ports/services/ |
| Application | DTO | <n>.dto.ts | application/dtos/ |
| Application | Use Case | <n>.usecase.ts | application/use-cases/ |
| Infrastructure | Repo Impl | <n>.repository.ts | infrastructure/adapters/repositories/ |
| Infrastructure | Presenter Impl | <n>.presenter.ts | infrastructure/adapters/presenters/ |
| Infrastructure | Controller | <n>.controller.ts | infrastructure/adapters/controllers/ |
| Infrastructure | Router Handler | <n>.router.handler.ts | infrastructure/routes/ |
| Infrastructure | Router | <n>.router.ts | infrastructure/routes/ |
Option 3 — Full CLEAN Architecture component
Generates the entire Clean Architecture structure in a single command. Every file is pre-filled with a functional TypeScript template ready to be adapted.
What is generated
╔══════════════════════════════════════════════════════════════════════════════╗
║ 🟠 INFRASTRUCTURE — wires everything together ║
║ adapters/controllers/<featureName>.controller.ts ║
║ adapters/repositories/<featureName>.repository.ts ║
║ adapters/presenters/<featureName>.presenter.ts ║
║ routes/<featureName>.router.handler.ts ║
║ routes/<featureName>.router.ts ║
║ ║
║ ╔══════════════════════════════════════════════════════════════════════╗ ║
║ ║ 🟦 APPLICATION — use cases & ports (interfaces) ║ ║
║ ║ ports/repositories/<featureName>.repository.interface.ts ║ ║
║ ║ ports/presenters/<featureName>.presenter.interface.ts ║ ║
║ ║ ports/services/<featureName>.service.ts ║ ║
║ ║ dtos/<featureName>.dto.ts ║ ║
║ ║ use-cases/<featureName>.usecase.ts ║ ║
║ ║ ║ ║
║ ║ ╔══════════════════════════════════════════════════════════════╗ ║ ║
║ ║ ║ 🟨 DOMAIN — pure business logic, no framework dependency ║ ║ ║
║ ║ ║ ║ ║ ║
║ ║ ║ ╔═══════════════════════════════════════════════════════╗ ║ ║ ║
║ ║ ║ ║ ⭐ ENTITIES (core — no external dependencies) ║ ║ ║ ║
║ ║ ║ ║ entities/<featureName>.entity.ts ║ ║ ║ ║
║ ║ ║ ╚═══════════════════════════════════════════════════════╝ ║ ║ ║
║ ║ ║ ║ ║ ║
║ ║ ║ events/<featureName>.event.ts ║ ║ ║
║ ║ ║ exceptions/<featureName>.exception.ts ║ ║ ║
║ ║ ╚══════════════════════════════════════════════════════════════╝ ║ ║
║ ╚══════════════════════════════════════════════════════════════════════╝ ║
╚══════════════════════════════════════════════════════════════════════════════╝
13 files · 12 directories — generated in a single interaction
← dependency direction: outer layers depend on inner layers, never the reverseExample — invoice feature
The only question asked during generation: the controller methods.
◆ Do you want to add methods to the controller?
│ ● Yes ○ No
└
◆ Enter the method names (comma separated):
│ create, findAll, findById, update, delete
└
✅ Entity created: src/features/invoice/domain/entities/invoice.entity.ts
✅ Event created: src/features/invoice/domain/events/invoice.event.ts
✅ Exception created: src/features/invoice/domain/exceptions/invoice.exception.ts
✅ Repository interface created: src/features/invoice/application/ports/repositories/invoice.repository.interface.ts
✅ Presenter interface created: src/features/invoice/application/ports/presenters/invoice.presenter.interface.ts
✅ Service interface created: src/features/invoice/application/ports/services/invoice.service.ts
✅ DTO created: src/features/invoice/application/dtos/invoice.dto.ts
✅ UseCase created: src/features/invoice/application/use-cases/invoice.usecase.ts
✅ Repository implementation: src/features/invoice/infrastructure/adapters/repositories/invoice.repository.ts
✅ Presenter implementation: src/features/invoice/infrastructure/adapters/presenters/invoice.presenter.ts
✅ Controller created: src/features/invoice/infrastructure/adapters/controllers/invoice.controller.ts
✅ Router Handler created: src/features/invoice/infrastructure/routes/invoice.router.handler.ts
✅ Router created: src/features/invoice/infrastructure/routes/invoice.router.ts
✅ register.router.ts updated with InvoiceRouter.
🎉 Feature "invoice" scaffolded with Clean Architecture!Generated content — invoice.entity.ts
/**
* InvoiceEntity — Domain Entity
* Represents the core business object for the "invoice" feature.
* No framework dependency, pure business logic only.
*/
export class InvoiceEntity {
private readonly _id: string;
private _createdAt: Date;
private _updatedAt: Date;
constructor(
id: string,
// TODO: Add your business properties here
createdAt?: Date,
updatedAt?: Date,
) {
this._id = id;
this._createdAt = createdAt ?? new Date();
this._updatedAt = updatedAt ?? new Date();
this.validate();
}
get id(): string { return this._id; }
get createdAt(): Date { return this._createdAt; }
get updatedAt(): Date { return this._updatedAt; }
public touch(): void {
this._updatedAt = new Date();
}
public toSnapshot(): Record<string, unknown> {
return { id: this._id, createdAt: this._createdAt, updatedAt: this._updatedAt };
}
private validate(): void {
if (!this._id || this._id.trim().length === 0) {
throw new Error(`[InvoiceEntity] id must not be empty.`);
}
// TODO: Add your domain invariant checks here
}
}Generated content — invoice.usecase.ts
import { IInvoiceRepository } from "../ports/repositories/invoice.repository.interface";
import { InvoiceEntity } from "../../domain/entities/invoice.entity";
import {
CreateInvoiceDto,
UpdateInvoiceDto,
InvoiceResponseDto,
InvoiceDtoMapper,
} from "../dtos/invoice.dto";
/**
* InvoiceUseCase — Application Use Case
*
* Orchestrates business operations for the "invoice" feature.
* Depends only on the repository port (interface), never on a concrete implementation.
*/
export class InvoiceUseCase {
constructor(private readonly repository: IInvoiceRepository) {}
async findAll(): Promise<InvoiceResponseDto[]> {
const entities = await this.repository.findAll();
return InvoiceDtoMapper.toResponseList(entities);
}
async findById(id: string): Promise<InvoiceResponseDto | null> {
const entity = await this.repository.findById(id);
if (!entity) return null;
return InvoiceDtoMapper.toResponse(entity);
}
async create(dto: CreateInvoiceDto): Promise<InvoiceResponseDto> {
const id = crypto.randomUUID();
const entity = new InvoiceEntity(id /* TODO: pass dto fields */);
const saved = await this.repository.create(entity);
return InvoiceDtoMapper.toResponse(saved);
}
async update(dto: UpdateInvoiceDto): Promise<InvoiceResponseDto | null> {
const existing = await this.repository.findById(dto.id);
if (!existing) return null;
existing.touch();
const updated = await this.repository.update(existing);
if (!updated) return null;
return InvoiceDtoMapper.toResponse(updated);
}
async delete(id: string): Promise<boolean> {
return this.repository.delete(id);
}
}Generated content — invoice.router.handler.ts (methods create, findAll, findById)
import { OpticoreRouting, ICustomContext, IMultipleRouteDefinition } from "opticore-router";
import { InvoiceController } from "../adapters/controllers/invoice.controller";
export const InvoiceHandlerRouter: () => IMultipleRouteDefinition = () => {
return OpticoreRouting.routes(
InvoiceController,
[
{
path: `/invoice`,
method: "post",
middlewares: [],
handler: async (ctx: ICustomContext) => await InvoiceController.create(ctx.req, ctx.res)
},
{
path: `/invoice`,
method: "get",
middlewares: [],
handler: async (ctx: ICustomContext) => await InvoiceController.findAll(ctx.req, ctx.res)
},
{
path: `/invoice/:id`,
method: "get",
middlewares: [],
handler: async (ctx: ICustomContext) => await InvoiceController.findById(ctx.req, ctx.res)
}
]
);
};Naming Rules
The feature name is subject to strict validation:
| Rule | Detail |
|---|---|
| Format | camelCase — ^[a-z][A-Za-z]+$ |
| First character | Must be lowercase |
| Minimum length | 2 characters |
| Allowed characters | Letters only (a-z, A-Z) |
| Forbidden characters | Digits, underscore, hyphen, spaces |
The CLI rejects the name if the feature already exists in src/features/.
Automatic Router Registration
When generating Simple Component and Full CLEAN Architecture, the feature router is automatically registered in src/app/router/register.router.ts.
Before:
export const registerRouter: () => TFeatureRoutes[] = (): TFeatureRoutes[] => {
return new OpticoreRegisterRouter().registered([
AuthenticationRouter,
]);
}After (InvoiceRouter added):
import { InvoiceRouter } from "../../features/invoice/infrastructure/routes/invoice.router";
export const registerRouter: () => TFeatureRoutes[] = (): TFeatureRoutes[] => {
return new OpticoreRegisterRouter().registered([
AuthenticationRouter,
InvoiceRouter,
]);
}If
register.router.tsis not found, a warning is displayed but generation continues normally.
In CLEAN by step mode, automatic registration is not performed because files are empty.
Options Summary
| Option | Files created | Content | Interaction | |---|---|---|---| | Simple Component | 6 | With template | Controller method names | | CLEAN by step | 0 to 13 (your choice) | Empty | Confirmation for each file | | Full CLEAN Architecture | 13 | With template | Controller method names |
Contributing
opticore-feature-component is open source.
To contribute: clone the repository and open a pull request.
- Repository: github.com/guyzoum77/opticore-feature-cli
- Issues: github.com/guyzoum77/opticore-feature-cli/issues
Author: Guy-serge Kouacou — MIT License
