npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

License: MIT npm version


Table of Contents


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

The register.router.ts file is automatically updated when a feature router is created.

If the features folder 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-component

Globally

npm install -g opticore-feature-component

From source (monorepo)

# from the package root
npm install
npm run build

Running the CLI

With npx or npm (no global install)

npx create-feature-module
npm exec create-feature-module

Via 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 feature

Global installation

create-feature-module

Global 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 · Organize

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

The 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 reverse

Example — 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.ts is 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.

Author: Guy-serge Kouacou — MIT License