"Because writing boilerplate code is like doing laundry - necessary, tedious, and something a machine should definitely handle for you."
A CLI code generator that transforms YAML specifications into fully functional TypeScript applications following clean architecture principles. Think of it as the overly enthusiastic intern who actually enjoys writing controllers, services, and domain models all day long.
npm install -g @currentjs/gen
# or use without installing
npx @currentjs/gen# Show help
currentjs --help
# Create a new app in the current directory
currentjs create app
# Create a new app inside a folder
currentjs create app my-app
# Create a module folder under src/modules
currentjs create module Blog
# Generate everything from app.yaml
currentjs generate
# Generate specific module
currentjs generate Blog --yaml app.yaml-
Create an empty app
currentjs create app # will create an app inside the current directory # or: currentjs create app my-project # will create a directory "my-project" and create an app there
-
Create a new module
currentjs create module Blog
-
Define your module's configuration in
src/modules/Blog/blog.yaml:- Define your data models
- Configure API routes & actions (CRUD is already configured)
- Set up permissions
-
Generate TypeScript files
currentjs generate Blog
-
Make custom changes (if needed) to:
- Business logic in models
- Custom actions in services
- HTML templates
-
Commit your changes to preserve them
currentjs commit
This generator takes your YAML specifications and creates:
- 🏗️ Complete app structure with TypeScript, configs, and dependencies
- 📋 Domain entities from your model definitions
- 🔄 Service layer with business logic and validation
- 🎭 Controllers for both API endpoints and web pages
- 💾 Data stores with database provider integration
- 🎨 HTML templates using @currentjs/templating
- 📊 Change tracking so you can modify generated code safely
See the HOW TO reference
currentjs create app [name] # Create new application
currentjs create module <name> # Create new module in existing appcurrentjs generate [module] # Generate code from YAML specs
--yaml app.yaml # Specify config file (default: ./app.yaml)
--force # Overwrite files without prompting
--skip # Skip conflicts, never overwriteThe generator includes a sophisticated change tracking system that revolutionizes how you work with generated code:
currentjs diff [module] # Show differences between generated and current code
currentjs commit [files...] # Commit your changes to version tracking
currentjs infer --file Entity.ts # Generate YAML model from existing TypeScript
--write # Write back to module YAML fileHere's the game-changer: You don't need to commit generated source code to your repository at all!
Instead, you only need to track:
- Your YAML files (the source of truth)
registry.json(change tracking metadata)- Your custom modifications (stored as reusable "patches")
Traditional Approach ❌
git/
├── src/modules/Blog/Blog.yaml # Source specification
├── src/modules/Blog/domain/entities/Post.ts # Generated + modified
├── src/modules/Blog/services/PostService.ts # Generated + modified
└── ... (hundreds of generated files with custom changes)
CurrentJS Approach ✅
git/
├── src/modules/Blog/Blog.yaml # Source specification
├── registry.json # Change tracking metadata
└── .currentjs/commits/ # Your custom modifications as patches
├── commit-2024-01-15.json
└── commit-2024-01-20.json
1. Initial Generation
currentjs generate
# Creates all files and tracks their hashes in registry.json2. Make Your Custom Changes
// Edit generated service to add custom logic
export class PostService extends GeneratedPostService {
async publishPost(id: number): Promise<void> {
// Your custom business logic here
const post = await this.getById(id);
post.publishedAt = new Date();
await this.update(id, post);
await this.sendNotificationEmail(post);
}
}3. Commit Your Changes
currentjs commit src/modules/Blog/services/PostService.ts
# Saves your modifications as reusable "hunks" (like git patches)4. Regenerate Safely
# Change your YAML specification
currentjs generate --force
# Your custom changes are automatically reapplied to the new generated code!# See what's different from generated baseline
currentjs diff BlogSample Output:
[modified] src/modules/Blog/services/PostService.ts
@@ -15,0 +16,8 @@
+
+ async publishPost(id: number): Promise<void> {
+ const post = await this.getById(id);
+ post.publishedAt = new Date();
+ await this.update(id, post);
+ await this.sendNotificationEmail(post);
+ }For Solo Development:
- Cleaner repositories (no generated code noise)
- Fearless regeneration (your changes are always preserved)
- Clear separation between specifications and implementations
For Team Development:
- Merge conflicts only happen in YAML files (much simpler)
- Team members can have different generated code locally
- Changes to business logic are tracked separately from schema changes
- New team members just run
currentjs generateto get up and running
For CI/CD:
# In your deployment pipeline
git clone your-repo
currentjs generate # Recreates all source code from YAML + patches
npm run deploySharing Customizations:
# Export your modifications
git add registry.json .currentjs/
git commit -m "Add custom publish functionality"
git push
# Teammates get your changes
git pull
currentjs generate # Their code automatically includes your customizationsThis change management system solves the age-old problem of "generated code vs. version control" by treating your customizations as first-class citizens while keeping your repository clean and merge-friendly!
For the cleanest repository experience, add this to your .gitignore:
# Generated source code (will be recreated from YAML + registry)
src/modules/*/domain/entities/*.ts
src/modules/*/application/services/*.ts
src/modules/*/application/validation/*.ts
src/modules/*/infrastructure/controllers/*.ts
src/modules/*/infrastructure/stores/*.ts
src/modules/*/infrastructure/interfaces/*.ts
# Keep these in version control
!*.yaml
!registry.json
!.currentjs/
# Standard Node.js ignores
node_modules/
build/
dist/
*.logWith this setup, your repository stays focused on what matters: your specifications and customizations, not generated boilerplate!
Working with multiple related models in a single module? CurrentJS now supports flexible endpoint configurations for multi-model scenarios.
Override the model for specific endpoints when you need to mix models within the same API or routes section:
models:
- name: Cat
fields:
- name: name
type: string
- name: Person
fields:
- name: name
type: string
- name: email
type: string
routes:
prefix: /cat
model: Cat # Default model for this section
endpoints:
- path: /create
action: empty
view: catCreate
# Uses Cat model (from default)
- path: /createOwner
action: empty
view: ownerCreate
model: Person # Override to use Person model for this endpointResult: The /cat/createOwner page will generate a form with Person fields (name, email), not Cat fields.
Organize endpoints by model or functionality using arrays:
# Multiple routes sections for different models
routes:
- prefix: /cat
model: Cat
endpoints:
- path: /
action: list
view: catList
- path: /create
action: empty
view: catCreate
- prefix: /person
model: Person
endpoints:
- path: /
action: list
view: personList
- path: /create
action: empty
view: personCreateResult: Generates separate controllers with their own base paths and endpoints.
If you don't specify a model, the system will infer it from the action handler:
routes:
prefix: /cat
endpoints:
- path: /createOwner
action: createOwner
view: ownerCreate
# No model specified - inferred from action below
actions:
createOwner:
handlers: [Person:default:create] # Infers Person modelPriority: endpoint.model → inferred from handler → section model → first model in array
Here's how you'd create a complete very simple blog system:
currentjs create app my-blog
cd my-blog
currentjs create module Blog# src/modules/Blog/Blog.yaml
models:
- name: Post
fields:
- name: title
type: string
required: true
- name: content
type: string
required: true
- name: authorEmail
type: string
required: true
- name: publishedAt
type: datetime
required: false
# this part is already generated for you!
api:
prefix: /api/posts
model: Post # Optional: specify which model this API serves
endpoints:
- method: GET
path: /
action: list
- method: POST
path: /
action: create
- method: GET
path: /:id
action: get
- method: PUT
path: /:id
action: update
- method: DELETE
path: /:id
action: delete
routes:
prefix: /posts
model: Post # Optional: specify which model this route serves
strategy: [back, toast]
endpoints:
- path: /
action: list
view: postList
- path: /:id
action: get
view: postDetail
- path: /create
action: empty
view: postCreate
- path: /:id/edit
action: get
view: postUpdate
actions:
list:
handlers: [Post:default:list]
get:
handlers: [Post:default:get]
create:
handlers: [Post:default:create]
update:
handlers: [Post:default:update]
delete:
handlers: [Post:default:delete]
permissions: []Note: All CRUD routes and configurations are automatically generated when you run
currentjs create module Blog. The only thing is left for you is your data model.
currentjs generate
npm startBoom! 💥 You now have a complete blog system with:
- REST API endpoints at
/api/posts/* - Web interface at
/posts/* - Full CRUD operations
- HTML templates for all views
- Database integration ready to go
my-app/
├── package.json # Dependencies (router, templating, providers)
├── tsconfig.json # TypeScript configuration
├── app.yaml # Main application config
├── src/
│ ├── app.ts # Main application entry point
│ ├── common/ # Shared utilities and templates
│ │ ├── services/ # Common services
│ │ └── ui/
│ │ └── templates/
│ │ ├── main_view.html # Main layout template
│ │ └── error.html # Error page template
│ └── modules/ # Your business modules
│ └── YourModule/
│ ├── YourModule.yaml # Module specification
│ ├── domain/
│ │ └── entities/ # Domain models
│ ├── application/
│ │ ├── services/ # Business logic
│ │ └── validation/ # Input validation
│ ├── infrastructure/
│ │ ├── controllers/ # HTTP endpoints
│ │ └── stores/ # Data access
│ └── views/ # HTML templates
├── build/ # Compiled JavaScript
└── web/ # Static assets, served as is
├── app.js # Frontend JavaScript
└── translations.json # i18n support
When you create a module, you'll work primarily with the ModuleName.yaml file. This file defines everything about your module:
src/modules/YourModule/
├── YourModule.yaml # ← This is where you define everything
├── domain/
│ └── entities/ # Generated domain models
├── application/
│ ├── services/ # Generated business logic
│ └── validation/ # Generated input validation
├── infrastructure/
│ ├── controllers/ # Generated HTTP endpoints
│ └── stores/ # Generated data access
└── views/ # Generated HTML templates
Here's a comprehensive example showing all available configuration options:
models:
- name: Post # Entity name (capitalized)
fields:
- name: title # Field name
type: string # Field type: string, number, boolean, datetime
required: true # Validation requirement
- name: content
type: string
required: true
- name: authorId
type: number
required: true
- name: publishedAt
type: datetime
required: false
- name: status
type: string
required: true
api: # REST API configuration
prefix: /api/posts # Base URL for API endpoints
endpoints:
- method: GET # HTTP method
path: / # Relative path (becomes /api/posts/)
action: list # Action name (references actions section)
- method: GET
path: /:id # Path parameter
action: get
- method: POST
path: /
action: create
- method: PUT
path: /:id
action: update
- method: DELETE
path: /:id
action: delete
- method: POST # Custom endpoint
path: /:id/publish
action: publish
routes: # Web interface configuration
prefix: /posts # Base URL for web pages
strategy: [toast, back] # Default success strategies for forms
endpoints:
- path: / # List page
action: list
view: postList # Template name
- path: /:id # Detail page
action: get
view: postDetail
- path: /create # Create form page
action: empty # No data loading action
view: postCreate
- path: /:id/edit # Edit form page
action: get # Load existing data
view: postUpdate
actions: # Business logic mapping
list:
handlers: [Post:default:list] # Use built-in list handler
get:
handlers: [Post:default:get] # Use built-in get handler
create:
handlers: [Post:default:create] # Built-in create
update:
handlers: [Post:default:update]
delete:
handlers: [ # Chain multiple handlers
Post:checkCanDelete, # Custom business logic
Post:default:delete
]
publish: # Custom action
handlers: [
Post:default:get, # Fetch entity
Post:validateForPublish, # Custom validation
Post:updatePublishStatus # Custom update logic
]
permissions: # Role-based access control
- role: all
actions: [list, get] # Anyone (including anonymous)
- role: authenticated
actions: [create] # Must be logged in
- role: owner
actions: [update, publish] # Entity owner permissions
- role: admin
actions: [update, delete, publish] # Admin role permissions
- role: editor
actions: [publish] # Editor role permissionsAvailable Field Types:
string- Text data (VARCHAR in database)number- Numeric data (INT/DECIMAL in database)boolean- True/false values (BOOLEAN in database)datetime- Date and time values (DATETIME in database)
Important Field Rules:
- Never include
id/owner_id/is_deletedfields - these are added automatically - Use
required: truefor mandatory fields - Use
required: falsefor optional fields
models:
- name: User
fields:
- name: email
type: string
required: true
- name: age
type: number
required: false
- name: isActive
type: boolean
required: true
- name: lastLoginAt
type: datetime
required: falseYou can define relationships between models by specifying another model name as the field type. The generator will automatically handle foreign keys, type checking, and UI components.
Basic Relationship Example:
models:
- name: Owner
fields:
- name: name
type: string
required: true
- name: email
type: string
required: true
- name: Cat
fields:
- name: name
type: string
required: true
- name: breed
type: string
required: false
- name: owner
type: Owner # Relationship to Owner model
required: trueArchitecture: Rich Domain Models
The generator uses Infrastructure-Level Relationship Assembly:
- Domain Layer: Works with full objects (no FKs)
- Infrastructure (Store): Handles FK ↔ Object conversion
- DTOs: Use FKs for API transmission
What Gets Generated:
-
Domain Model: Pure business objects
import { Owner } from './Owner'; export class Cat { constructor( public id: number, public name: string, public breed?: string, public owner: Owner // ✨ Full object, no FK! ) {} }
-
DTOs: Use foreign keys for API
export interface CatDTO { name: string; breed?: string; ownerId: number; // ✨ FK for over-the-wire }
-
Store: Converts FK ↔ Object
export class CatStore { constructor( private db: ISqlProvider, private ownerStore: OwnerStore // ✨ Foreign store dependency ) {} async loadRelationships(entity: Cat, row: CatRow): Promise<Cat> { const owner = await this.ownerStore.getById(row.ownerId); if (owner) entity.setOwner(owner); return entity; } async insert(cat: Cat): Promise<Cat> { const row = { name: cat.name, ownerId: cat.owner?.id // ✨ Extract FK to save }; // ... } }
-
Service: Loads objects from FKs
export class CatService { constructor( private catStore: CatStore, private ownerStore: OwnerStore // ✨ To load relationships ) {} async create(catData: CatDTO): Promise<Cat> { // ✨ Load full owner object from FK const owner = await this.ownerStore.getById(catData.ownerId); const cat = new Cat(0, catData.name, catData.breed, owner); return await this.catStore.insert(cat); } }
-
HTML Forms: Select dropdown with "Create New" button
<select id="ownerId" name="ownerId" required> <option value="">-- Select Owner --</option> <!-- Options loaded from /api/owner --> </select> <button onclick="window.open('/owner/create')">+ New</button>
Relationship Naming Convention:
The generator automatically creates foreign key fields following this convention:
- Field name:
owner→ Foreign key:ownerId - Field name:
author→ Foreign key:authorId - Field name:
parentComment→ Foreign key:parentCommentId
The foreign key always references the id field of the related model.
Optional Configuration:
models:
- name: Post
fields:
- name: title
type: string
required: true
- name: author
type: User
required: true
displayFields: [name, email] # Fields to show in dropdowns (optional)Configuration Options:
displayFields: Array of fields from the related model to display in UI dropdowns (optional, defaults to showing the ID)
Multiple Relationships:
models:
- name: Comment
fields:
- name: content
type: string
required: true
- name: post
type: Post # Creates foreign key: postId
required: true
- name: author
type: User # Creates foreign key: authorId
required: true
displayFields: [username, email]
- name: parentComment
type: Comment # Self-referential relationship
required: false # Creates foreign key: parentCommentIdRelationship Best Practices:
- ✅ Always define the foreign model first in the same module
- ✅ Use descriptive field names for relationships (e.g.,
authorinstead ofuser) - ✅ Set appropriate
displayFieldsto show meaningful data in dropdowns - ✅ Use
required: falsefor optional relationships - ✅ Foreign keys are auto-generated following the pattern
fieldName + 'Id' - ❌ Don't manually add foreign key fields (they're auto-generated)
- ❌ Don't create circular dependencies between modules
🔄 Handler vs Action Distinction:
- Handler: Creates a separate service method (one handler = one method)
- Action: Virtual controller concept that calls handler methods step-by-step
Built-in Handlers:
ModelName:default:list- Creates service method with pagination parametersModelName:default:get- Creates service method namedgetwith ID parameterModelName:default:create- Creates service method with DTO parameterModelName:default:update- Creates service method with ID and DTO parametersModelName:default:delete- Creates service method with ID parameter
Custom Handlers:
ModelName:customMethodName- Creates service method that acceptsresult, contextparametersresult: Result from previous handler (ornullif it's the first handler)context: The request context object- User can customize the implementation after generation
- Each handler generates a separate method in the service
🔗 Multiple Handlers per Action: When an action has multiple handlers, each handler generates a separate service method, and the controller action calls them sequentially:
actions:
get:
handlers:
- Invoice:default:get # Creates Invoice.get() method
- Invoice:enrichData # Creates Invoice.enrichData() methodGenerated Code Example:
// InvoiceService.ts
async get(id: number): Promise<Invoice> {
// Standard get implementation
}
async enrichData(result: any, context: any): Promise<any> {
// TODO: Implement custom enrichData method
// result = result from previous handler (Invoice object in this case)
// context = request context
}
// InvoiceController.ts
async get(context: IContext): Promise<Invoice> {
const id = parseInt(context.request.parameters.id as string);
const result1 = await this.invoiceService.get(id);
const result = await this.invoiceService.enrichData(result1, context);
return result; // Returns result from last handler
}Parameter Passing Rules:
- Default handlers (
:default:): Receive standard parameters (id, pagination, DTO, etc.) - Custom handlers: Receive
(result, context)where:result: Result from previous handler, ornullif it's the first handlercontext: Request context object
Handler Format Examples:
actions:
list:
handlers: [Post:default:list] # Single handler: list(page, limit)
get:
handlers: [Post:default:get] # Single handler: get(id)
complexFlow:
handlers: [
Post:default:create, # create(userData) - standard parameters
Post:sendNotification, # sendNotification(result, context) - result from create
Comment:default:create # create(userData) - standard parameters
]
customFirst:
handlers: [
Post:validateInput, # validateInput(null, context) - first handler
Post:default:create # create(userData) - standard parameters
]When you have multiple models in a single module, the system generates individual services, controllers, and stores for each model:
models:
- name: Post
fields: [...]
- name: Comment
fields: [...]
api:
model: Post # This API serves the Post model
prefix: /api/posts
endpoints: [...]
actions:
createPost:
handlers: [Post:default:create] # Calls PostService.create()
addComment:
handlers: [Comment:default:create] # Calls CommentService.create()
publishPost:
handlers: [
Post:validateContent, # Calls PostService.validateContent()
Comment:notifySubscribers # Calls CommentService.notifySubscribers()
]Key Points:
- Each model gets its own Service, Controller, and Store classes
- Use
modelparameter to specify which model an API/route serves (defaults to first model) - Use
ModelName:default:actionfor built-in operations (list, create, etc.) - Use
ModelName:customMethodfor custom service methods - You can mix actions across different models in a single handler chain
Configure what happens after successful form submissions:
routes:
strategy: [toast, back] # Show success message and go backAvailable Strategies:
toast- Success toast notification (most common)back- Navigate back in browser historymessage- Inline success messagemodal- Modal success dialogredirect- Redirect to specific URLrefresh- Reload current page
Common Strategy Combinations:
# Most common: Show message and go back
strategy: [toast, back]
# Show message and stay on page
strategy: [toast]
# Show modal dialog
strategy: [modal]
# Multiple feedback
strategy: [toast, modal, back]Control who can access what actions:
permissions:
- role: all
actions: [list] # Anyone (including anonymous)
- role: authenticated
actions: [create] # Any logged-in user
- role: owner
actions: [update] # Owner of entity
- role: admin
actions: [update, delete] # Admin permissionsSpecial Roles:
all- Everyone (including anonymous users)authenticated- Any logged-in userowner- User who created the entityadmin,editor,user- Custom roles from JWT token
How Ownership Works:
The system automatically adds an owner_id field to track who created each entity. When you use owner role, it checks if the current user's ID matches the entity's owner_id.
✅ Use YAML Configuration For:
- Basic CRUD operations
- Standard REST endpoints
- Simple permission rules
- Form success strategies
- Standard data field types
✅ Write Custom Code For:
- Complex business logic
- Custom validation rules
- Data transformations
- Integration with external services
- Complex database queries
This generator is the foundation of the currentjs framework:
- Works seamlessly with
@currentjs/routerfor HTTP handling - Integrates with
@currentjs/templatingfor server-side rendering - Uses
@currentjs/provider-*packages for database access - Follows clean architecture principles for maintainable code
currentjs create appscaffolds complete app structure with TypeScript configs and dependenciescurrentjs generatecreates domain entities, services, controllers, stores, and templates- Generated code follows clean architecture: domain/application/infrastructure layers
- Supports both API endpoints and web page routes in the same module
- Includes change tracking system for safely modifying generated code
Vibecoded with claude-4-sonnet (mostly) by Konstantin Zavalny. Yes, it is a vibecoded solution, really.
Any contributions such as bugfixes, improvements, etc are very welcome.
GNU Lesser General Public License (LGPL)
It simply means, that you:
- can create a proprietary application that uses this library without having to open source their entire application code (this is the "lesser" aspect of LGPL compared to GPL).
- can make any modifications, but must distribute those modifications under the LGPL (or a compatible license) and include the original copyright and license notice.