A Laravel-inspired Node.js framework for building production-ready REST APIs — structured, expressive, and batteries-included.
pnpm create lara-node my-api- Expressive routing — fluent route builder with named middleware, groups, and parameter constraints
- Eloquent-style ORM — models with relationships, traits (SoftDeletes, Timestamps, Cacheable…), and Mongo/MySQL support
- Service container — IoC container with auto-injection and service providers
- Global
request()— the current request anywhere via async context; referrer, headers, negotiation, client info, and route helpers - Global config system — dot-notation
config()accessor; packages set defaults, app overrides viaConfigServiceProvider - Middleware service provider — register aliases (
auth,can,role), groups, and priority order in a dedicated provider - Class-based middleware — pass constructor classes directly; the engine calls
handle()automatically - Event system — synchronous and queued events, listeners, subscribers, and WebSocket broadcasting
- Job queue — driver-based queue (sync, MongoDB, Redis) with retries, priorities, and failed-job tracking
- Per-app queue namespacing — several services share one Redis safely; address a sibling's queue with
queue@app - Cron scheduler — fluent task scheduling with distributed locking
- Mail — SMTP, log, and array drivers with Mailable classes; runtime config via
config('mail.*') - Artisan CLI — 50+ built-in commands; add your own with a single class
artisan serve— auto port fallback (tries next port sequentially if current one is in use)vendor:publish— copies package config templates tosrc/config/for app-level customization- Telescope — request, query, job, log, exception, and cache watcher with a live dashboard
- Horizon — queue supervisor dashboard with auto-scaling workers
- OpenAPI docs — auto-generated Swagger UI from route and controller decorators
- Node.js 20+
- pnpm 9+
- MongoDB or MySQL
pnpm create lara-node my-api
cd my-api
cp .env.example .env # set DB_CONNECTION, DB_NAME, JWT_SECRET
pnpm install
pnpm artisan key:generate --write
pnpm artisan migrate
pnpm artisan serveServer starts at http://localhost:3000 (auto-increments if port is busy).
API docs at http://localhost:3000/docs.
| Package | Version | Description |
|---|---|---|
@lara-node/core |
0.1.4 | IoC container, Application, ServiceProvider, global config, MiddlewareStack |
@lara-node/db |
0.1.2 | ORM models, relationships, migrations, schema builder |
@lara-node/router |
0.1.3 | Route builder, middleware stack, HTTP kernel, OpenAPI |
@lara-node/auth |
0.1.2 | JWT auth, bcrypt helpers |
@lara-node/cache |
0.1.2 | Cache drivers (file, Redis, DB, memory), rate limiter |
@lara-node/events |
0.1.3 | Event dispatcher, listeners, subscribers, broadcasting |
@lara-node/queue |
0.1.3 | Job queue, scheduler, workers |
@lara-node/mail |
0.1.3 | Mailers, drivers (SMTP, log, array) |
@lara-node/console |
0.1.5 | Artisan CLI kernel, 50+ commands, vendor:publish |
@lara-node/middlewares |
0.1.7 | Built-in middleware classes (Auth, RBAC, Throttle…) |
@lara-node/validator |
0.1.6 | Laravel-style validation rules |
@lara-node/horizon |
0.1.4 | Queue supervisor + dashboard |
@lara-node/telescope |
0.1.4 | Debug & observability dashboard |
create-lara-node |
0.2.4 | Project scaffolding (pnpm create lara-node) |
All packages publish with semver ranges (^x.y.z) on their internal dependencies so pnpm resolves a single shared instance of @lara-node/core in every project.
my-api/
├── src/
│ ├── server.ts # HTTP entry point
│ ├── artisan.ts # CLI entry point
│ ├── register.ts # tsconfig-paths + reflect-metadata bootstrap
│ ├── bootstrap/
│ │ └── app.ts # Application bootstrap & boot sequences
│ ├── routes/
│ │ ├── api.ts # API routes
│ │ └── web.ts # Web routes
│ ├── app/
│ │ ├── Http/
│ │ │ ├── Kernel.ts # Global middleware pipeline
│ │ │ ├── Controllers/ # Route controllers
│ │ │ └── Middleware/ # Custom middleware classes
│ │ ├── Models/ # Eloquent models
│ │ ├── Events/ # Event classes
│ │ ├── Listeners/ # Event listeners
│ │ ├── Jobs/ # Queued jobs
│ │ ├── Mail/ # Mailable classes
│ │ ├── Providers/
│ │ │ ├── AppServiceProvider.ts # Root provider — cascades to all others
│ │ │ ├── ConfigServiceProvider.ts # Loads src/config/* and overrides package defaults
│ │ │ ├── MiddlewareServiceProvider.ts # Registers aliases, groups, priority
│ │ │ ├── RouteServiceProvider.ts # Boots route files
│ │ │ ├── EventServiceProvider.ts
│ │ │ ├── BroadcastServiceProvider.ts
│ │ │ └── QueueServiceProvider.ts
│ │ └── Console/Commands/ # Custom artisan commands
│ ├── config/ # App-level config overrides (see vendor:publish)
│ │ ├── app.config.ts
│ │ ├── db.config.ts
│ │ ├── mail.config.ts
│ │ ├── queue.config.ts
│ │ └── broadcasting.config.ts
│ └── database/
│ ├── migrations/ # Class-based migration files
│ └── seeders/ # Database seeders
├── .env
└── package.json
One container serves the whole application. Anything it builds gets its constructor dependencies resolved — controllers, console commands, queued jobs, event listeners and subscribers:
@Injectable()
export class SendPurchaseReceipt {
constructor(private readonly mailer: MailService) {}
async handle(payload: { purchaseId: string }) {
await this.mailer.send(new ReceiptMail(payload.purchaseId));
}
}Services and plain data can share a constructor. Parameters the container
cannot build — a number, a string, an interface — are passed as
undefined, so their defaults apply:
constructor(private readonly outbox: Outbox, public batchSize = 100) {}Injected services are never written into a queued job's payload. See
@lara-node/core.
Providers bootstrap your application. They run in order:
ConfigServiceProvider— loadssrc/config/*and callssetConfig()to override package defaultsMiddlewareServiceProvider— registers named aliases, groups, and priorityRouteServiceProvider— lazily boots route files (middleware aliases must be ready first)
// src/app/Providers/AppServiceProvider.ts
import { ServiceProvider, ServiceProviderClass } from '@lara-node/core';
import { ConfigServiceProvider } from './ConfigServiceProvider';
import { MiddlewareServiceProvider } from './MiddlewareServiceProvider';
import { RouteServiceProvider } from './RouteServiceProvider';
export class AppServiceProvider extends ServiceProvider {
protected additionalProviders: ServiceProviderClass[] = [
ConfigServiceProvider, // 1. configs first
MiddlewareServiceProvider, // 2. middleware aliases
RouteServiceProvider, // 3. routes last
];
register(): void {
this.registerProviders(this.additionalProviders);
this.singleton(PaymentService);
}
}Every @lara-node/* package registers its default config at import time via setConfig(). Your ConfigServiceProvider overrides those defaults with app-level values from src/config/.
import { config, setConfig } from '@lara-node/core';
// Read with dot-notation (works anywhere after providers have run)
const driver = config('mail.default'); // 'smtp'
const host = config('mail.mailers.smtp.host'); // 'smtp.mailgun.org'
const appName = config('app.name', 'MyApp'); // fallback if unset
// Override a namespace (usually done inside a ServiceProvider)
setConfig('mail', { default: 'ses', mailers: { ... }, from: { ... } });ConfigServiceProvider — generated automatically by pnpm create lara-node:
// src/app/Providers/ConfigServiceProvider.ts
import { ServiceProvider, setConfig } from '@lara-node/core';
import appConfig from '../../config/app.config';
import mailConfig from '../../config/mail.config';
import queueConfig from '../../config/queue.config';
export class ConfigServiceProvider extends ServiceProvider {
register(): void {
setConfig('app', appConfig as unknown as Record<string, unknown>);
setConfig('mail', mailConfig as unknown as Record<string, unknown>);
setConfig('queue', queueConfig as unknown as Record<string, unknown>);
}
}Publish config templates from installed packages into src/config/:
pnpm artisan vendor:publish --tag=configThis scans every @lara-node/* package for a laraNode.publish manifest and copies the matching template files. Use --force to overwrite existing files, --list to preview without copying, and --provider mail to target a single package.
Register all named middleware in one place. This provider extends MiddlewareServiceProvider from @lara-node/core:
// src/app/Providers/MiddlewareServiceProvider.ts
import { MiddlewareServiceProvider as BaseProvider } from '@lara-node/core';
import {
AuthMiddleware,
AuthorizeByStatusMiddleware,
authorizeRoles,
authorizePermissions,
} from '@lara-node/middlewares';
export class MiddlewareServiceProvider extends BaseProvider {
protected registerMiddleware(): void {
// Named aliases — use in routes as strings: 'auth', 'can:perm', 'role:admin'
this.middlewareAliases({
auth: new AuthMiddleware({ userLoader: async (uid) => User.find(uid) }).toHandler(),
'must-be-active': AuthorizeByStatusMiddleware, // class — handle() called automatically
can: (...perms: string[]) => authorizePermissions(...perms),
role: (...roles: string[]) => authorizeRoles(...roles),
});
// Middleware groups
this.middlewareGroup('web', []);
this.middlewareGroup('api', [/* 'throttle:120,1' */]);
// Execution priority when multiple aliases apply to the same route
this.middlewarePriority(['auth', 'must-be-active', 'can', 'role']);
}
}Available registration helpers (all on ServiceProvider):
| Method | Description |
|---|---|
middlewareAlias(name, entry) |
Register a single alias |
middlewareAliases(map) |
Register multiple aliases at once |
middlewareGroup(name, list) |
Define a named group |
appendMiddlewareToGroup(group, mw) |
Add to the end of a group |
prependMiddlewareToGroup(group, mw) |
Add to the front of a group |
removeMiddlewareFromGroup(group, mw) |
Remove from a group |
middlewarePriority(list) |
Set execution priority order |
singletonMiddleware(mw) |
Instantiate a class only once |
The Kernel defines the global middleware pipeline — every request passes through these before hitting a route. Named aliases and groups live in MiddlewareServiceProvider, not here.
// src/app/Http/Kernel.ts
import { HttpKernel as BaseKernel, middlewareStack } from '@lara-node/router';
import type { Middleware } from '@lara-node/core';
import type { ErrorRequestHandler, RequestHandler } from 'express';
import {
AsyncContextMiddleware,
RequestLoggerMiddleware,
RequestExtenderMiddleware,
ValidatorMiddleware,
ResponseExtenderMiddleware,
ErrorHandlerMiddleware,
} from '@lara-node/middlewares';
export class Kernel extends BaseKernel {
// Pass classes directly — the stack calls handle() automatically
protected override middleware: RequestHandler[] = middlewareStack.resolveMiddlewareStack([
AsyncContextMiddleware, // establishes the context request() reads
RequestLoggerMiddleware,
RequestExtenderMiddleware, // attaches the accessors to req
ValidatorMiddleware,
ResponseExtenderMiddleware,
] as Middleware[]);
readonly errorHandler: ErrorRequestHandler = (err, req, res, next) =>
new ErrorHandlerMiddleware().handle(err, req, res, next);
}request() returns the current request from anywhere — services, models, event
listeners, policies — without threading req through every call. It resolves
from the async request context established by AsyncContextMiddleware, so it
works at any depth below it.
Outside an HTTP request (queue workers, artisan commands) it returns null, so
guard it:
import { request, requestOrFail } from '@lara-node/core';
// or use the globals — registered automatically, no import needed
const ref = request()?.referrer();
const agent = request()?.headers('user-agent');
const apiKey = request()?.headers('x-api-key', 'none');
const ip = request()?.clientIp();
const id = request()?.requestId();
// Throws instead of returning null — for code only reachable over HTTP
const req = requestOrFail();headers is both callable and indexable, so either style reads naturally:
request()?.headers('referer') // one header, case-insensitive
request()?.headers('x-key', 'na') // with a default
request()?.headers() // all of them
request()?.headers['user-agent'] // index style still worksRequestExtenderMiddleware attaches the same accessors to the express req,
backed by the same object — req.referrer() and request()!.referrer() are one
code path. Where express already owns a name (header(), accepts(), is(),
ips), reach the wrapper through req.lara().
| Section | Methods |
|---|---|
| Referrer / origin | referrer() referer() referrerHost() origin() isCrossOrigin() |
| Headers | headers(key?) header() hasHeader() requestId() |
| Content | contentType() charset() contentLength() |
| Negotiation | accepts() acceptsJson() acceptsHtml() acceptableContentTypes() languages() language() preferredLanguage() encodings() |
| Method | isGet() isPost() isPut() isPatch() isDelete() isHead() isOptions() isMethodSafe() isMethodIdempotent() |
| URL | fullUrl() port() queryString() is() fullUrlIs() segments() segment() |
| Route | route() routeParam() |
| Client | clientIp() ips() userAgent() isBot() isMobile() fingerprint() |
| Credentials | bearerToken() basicAuth() user() |
| Cookies | cookies(key?) cookie() hasCookie() |
| Input | all() input() only() except() string() integer() boolean() date() collect() has() filled() merge() |
| Timing | startedAt() elapsed() |
| Context | store(key?, value?) — read/write the async request store |
FormRequest extends this class, so every method above is available on a
validated form request too.
Routes are defined with a fluent RouterBuilder. Use string aliases registered in MiddlewareServiceProvider:
// src/routes/api.ts
import RouterBuilder from '@lara-node/router';
import { AuthController } from '@app/Http/Controllers/User/AuthController';
import { PostController } from '@app/Http/Controllers/PostController';
const { group, prefix } = new RouterBuilder();
// Public routes
group('/auth', (g) => {
g.post('/register', [AuthController, 'register']);
g.post('/login', [AuthController, 'login']);
});
// Protected routes — 'auth' alias resolved from MiddlewareServiceProvider
group('/posts', (g) => {
g.get('/', 'auth', [PostController, 'index']);
g.post('/', 'auth', [PostController, 'store']);
g.get('/:id', 'auth', [PostController, 'show']);
g.put('/:id', 'auth', 'can:edit_post', [PostController, 'update']);
g.delete('/:id', 'auth', 'role:admin', [PostController, 'destroy']);
});Mount in RouteServiceProvider.boot():
async boot(): Promise<void> {
const { apiRouter } = await import('../../routes/api');
this.app.mountRoutes('/api', apiRouter.build());
}Models extend the Model base class. Define your collection/table name, fillable fields, and relationships.
Attributes read as plain properties — user.email — from outside the model and
from inside its own methods and getters alike. A stored attribute takes priority
over a same-named getter, so name derived accessors distinctly (environment,
not env).
import { Model, HasMany, SoftDeletes, Timestamps, applyTraits } from '@lara-node/db';
import type { Post } from './Post';
@applyTraits(Timestamps, SoftDeletes)
export class User extends Model {
static collectionName = 'users';
static fillable = ['name', 'email', 'password'];
static hidden = ['password'];
posts(): HasMany<Post> {
return this.hasMany('Post', 'user_id');
}
}Querying:
const user = await User.find(id);
const admins = await User.where('role', 'admin').orderBy('name').limit(10).get();
const post = await Post.create({ title: 'Hello', user_id: user.id });
await post.update({ title: 'Updated' });
await post.delete(); // soft-delete if SoftDeletes trait applied
await post.forceDelete(); // hard deleteRelationships:
const user = await User.find(1);
const posts = await user.posts().get();
await user.posts().create({ title: 'My Post' });Dispatch events synchronously or queue them for background processing.
// src/app/Events/PostCreated.ts
import { Event } from '@lara-node/events';
export class PostCreated extends Event {
constructor(public post: Record<string, unknown>) { super(); }
}// src/app/Listeners/NotifyFollowers.ts
import { Listener, ListensTo } from '@lara-node/events';
import type { PostCreated } from '../Events/PostCreated';
@ListensTo(PostCreated)
export class NotifyFollowers extends Listener<PostCreated> {
async handle(event: PostCreated): Promise<void> {
// send notification
}
}Dispatch from anywhere:
import { event } from '@lara-node/events';
await event(new PostCreated(post));Define a job class, dispatch it, and run a worker.
// src/app/Jobs/SendWelcomeEmail.ts
import { Job } from '@lara-node/queue';
export class SendWelcomeEmail extends Job {
constructor(public userId: string) { super(); }
async handle(): Promise<void> {
// send the email
}
}import { dispatch } from '@lara-node/queue';
// Fire and forget
await dispatch(new SendWelcomeEmail(user.id));
// With delay and queue name
await dispatch(new SendWelcomeEmail(user.id))
.delay(60)
.onQueue('notifications')
.dispatch();pnpm artisan queue:work
pnpm artisan queue:work --connection redis --queue high,defaultEvery Redis key the queue writes is namespaced by the owning application, so
two Lara-Node services pointed at the same Redis instance never pop each
other's jobs. The namespace resolves from QUEUE_APP, then APP_NAME —
set one per service, or both fall back to app and collide.
billing_queue:invoices # billing's pending jobs
billing_queue:invoices:delayed:score # billing's delayed set
billing_queue:invoices:reserved # billing's in-flight jobs
billing_queue:failed_jobs # billing's failures
search_queue:index # search's — a separate key entirely
Worker restart signals, scheduler locks and the maintenance flag are scoped the
same way, so queue:restart on one service leaves the others running.
To address a sibling service's queue on purpose, suffix the queue name with
@app:
// Dispatch onto search's 'index' queue from billing
await Queue.push(new ReindexJob({ id }), 'index@search');
await dispatch(new ReindexJob({ id })).onQueue('index@search').dispatch();# Or work another service's queue
pnpm artisan queue:work redis --queue=index@searchFailed jobs follow the queue's owner, so a failure on index@search lands in
search's failed-job hash where search's queue:retry can recover it.
Configure it in config/queue.config.ts:
redis: {
driver: 'redis',
app: process.env.QUEUE_APP ?? process.env.APP_NAME, // → 'billing_queue'
prefix: process.env.REDIS_PREFIX ?? undefined, // override outright
// Only needed for siblings whose prefix is not '<app>_queue'
apps: { search: 'search-jobs' },
}The default prefix shape is <app>_queue, unchanged from previous versions, so
upgrading does not orphan jobs already sitting in Redis.
Configuration files hold data only — the types live in
@lara-node/queue's types.ts and the namespacing behaviour in
namespace.ts, so editing a published config can never break them.
Define scheduled tasks in your QueueServiceProvider.boot():
import { scheduler } from '@lara-node/queue';
scheduler.call(() => cleanupExpiredSessions(), '0 2 * * *')
.description('Clean expired sessions')
.timezone('UTC');
scheduler.command('cache:clear').everyHour();pnpm artisan schedule:work # long-running daemon
pnpm artisan schedule:run # run due tasks once (for crontab)
pnpm artisan schedule:list # show upcoming tasks// src/app/Mail/WelcomeMail.ts
import { Mailable } from '@lara-node/mail';
export class WelcomeMail extends Mailable {
constructor(private name: string) { super(); }
build() {
return this.subject('Welcome!')
.html(`<h1>Hi ${this.name}, welcome!</h1>`);
}
}import { Mail } from '@lara-node/mail';
// Send immediately
await Mail().mailer().to('user@example.com').send(new WelcomeMail(user.name));
// Queue for background delivery
await Mail().mailer().to('user@example.com').queue(new WelcomeMail(user.name));
// Send with delay (seconds)
await Mail().mailer().to('user@example.com').later(new WelcomeMail(user.name), 300);Mail config is driven by src/config/mail.config.ts (publish with vendor:publish --tag=config). The config('mail.default') accessor is used internally, so runtime overrides work without rebuilding.
import { Cache } from '@lara-node/cache';
await Cache.put('user:1', userData, 300); // 300s TTL
const user = await Cache.get('user:1');
await Cache.forget('user:1');
await Cache.remember('user:1', 300, () => User.find(1)); // get or compute
await Cache.has('user:1');Driver is read from config('cache.driver') (set in src/config/app.config.ts).
All configuration is environment-driven via .env. Config files in src/config/ map env vars to typed objects and register them with the global config() system via ConfigServiceProvider.
APP_NAME=my-api
APP_ENV=local
APP_KEY=base64:... # pnpm artisan key:generate --write
APP_DEBUG=true
PORT=3000
# Database
DB_CONNECTION=mongodb # mongodb | mysql
DB_HOST=127.0.0.1
DB_PORT=27017
DB_NAME=my_api
DB_USER=root
DB_PASSWORD=
# Cache
CACHE_DRIVER=file # file | memory | redis | db
# Queue
QUEUE_CONNECTION=sync # sync | database | redis
QUEUE_APP=my-api # Redis key namespace — unique per service
# REDIS_PREFIX= # overrides the derived '<app>_queue' prefix
# REDIS_URL=redis://127.0.0.1:6379
# REDIS_HOST=127.0.0.1
# REDIS_PORT=6379
# REDIS_USERNAME=
# REDIS_PASSWORD=
# REDIS_DB=0
# REDIS_QUEUE=default
# Mail
MAIL_MAILER=smtp # smtp | log | array
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME=my-api
# Broadcasting
BROADCAST_DRIVER=websocket # websocket | redis | log | null
# JWT
JWT_SECRET=change-me
# Telescope (optional)
TELESCOPE_ENABLED=true
TELESCOPE_TOKEN=secret # protect /telescope in production
# Horizon (optional)
HORIZON_ENABLED=true
HORIZON_TOKEN=secretReading config in code:
import { config } from '@lara-node/core';
config('app.name') // → 'my-api'
config('mail.default') // → 'smtp'
config('queue.connections.redis.driver') // → 'redis'
config('nonexistent', 'fallback') // → 'fallback'pnpm artisan serve # start server, auto-selects next port if busy
pnpm artisan serve --port 8080 # explicit port
pnpm artisan serve --host 0.0.0.0 # bind to all interfacesIf the requested port is already in use, artisan automatically tries the next port sequentially (3000 → 3001 → 3002 …) and prints which port it started on.
pnpm artisan key:generate # print a new APP_KEY
pnpm artisan key:generate --write # write it to .envpnpm artisan migrate # run pending migrations
pnpm artisan migrate:fresh # drop all + re-migrate
pnpm artisan migrate:rollback # roll back last batch
pnpm artisan migrate:status # show migration state
pnpm artisan db:seed # run database seeders
pnpm artisan db:wipe # drop all tables/collections
pnpm artisan make:migration create_orders_table
pnpm artisan make:seeder OrderSeederpnpm artisan vendor:publish # publish all tags from all packages
pnpm artisan vendor:publish --tag=config # only config files
pnpm artisan vendor:publish --provider=mail # only from @lara-node/mail
pnpm artisan vendor:publish --force # overwrite existing files
pnpm artisan vendor:publish --list # preview without copyingAfter running --tag=config, edit the files in src/config/ to customize behaviour. ConfigServiceProvider picks them up automatically on next boot.
pnpm artisan queue:work
pnpm artisan queue:work --connection redis --queue high,default
pnpm artisan queue:listen
pnpm artisan queue:status
pnpm artisan queue:failed
pnpm artisan queue:retry <id>
pnpm artisan queue:flush # clear the failed jobs table
pnpm artisan queue:clear # clear pending jobspnpm artisan schedule:work # long-running daemon
pnpm artisan schedule:run # run due tasks once (crontab mode)
pnpm artisan schedule:list # list scheduled taskspnpm artisan cache:clear
pnpm artisan cache:get <key>
pnpm artisan cache:set <key> <value>
pnpm artisan cache:forget <key>
pnpm artisan cache:has <key>
pnpm artisan cache:list
pnpm artisan cache:driverpnpm artisan event:list
pnpm artisan event:dispatch user.registered --payload '{"id":1}'
pnpm artisan event:clear
pnpm artisan make:event OrderPlaced
pnpm artisan make:listener SendOrderConfirmation --event OrderPlaced
pnpm artisan make:subscriber OrderSubscriberpnpm artisan broadcast:channels
pnpm artisan broadcast:connections
pnpm artisan broadcast:send <event> <channels>
pnpm artisan broadcast:terminate <connection-id>pnpm artisan horizon:work
pnpm artisan horizon:status
pnpm artisan horizon:pause
pnpm artisan horizon:continue
pnpm artisan horizon:terminate
pnpm artisan horizon:listpnpm artisan docs:generate # write openapi.json
pnpm artisan docs:routes # list documented routes// src/app/Console/Commands/GreetCommand.ts
import { Command } from '@lara-node/console';
import type { ArgumentsCamelCase } from 'yargs';
export class GreetCommand extends Command {
protected signature = 'greet <name>';
protected description = 'Greet a user by name';
protected options = {
shout: { type: 'boolean' as const, alias: 's', default: false, description: 'UPPERCASE output' },
};
async handle(args: ArgumentsCamelCase): Promise<void> {
const greeting = `Hello, ${args.name}!`;
this.success(this.option<boolean>('shout') ? greeting.toUpperCase() : greeting);
}
}Register by adding it to your artisan kernel or AppServiceProvider:
this.singleton(GreetCommand);Available output helpers: this.info(), this.success(), this.warn(), this.error(), this.line(), this.comment(), this.newLine().
Request, query, job, log, cache, and exception watcher with a live dashboard.
# Dashboard at http://localhost:3000/telescopeEnable in AppServiceProvider.additionalProviders:
import { TelescopeServiceProvider } from '@lara-node/telescope';
protected additionalProviders: ServiceProviderClass[] = [
// ...
TelescopeServiceProvider,
];Publish the config template to customise watchers:
pnpm artisan vendor:publish --tag=config --provider=telescopeQueue supervisor with auto-scaling and a metrics dashboard.
pnpm artisan horizon:work
# Dashboard at http://localhost:3000/horizonEnable via additionalProviders:
import { HorizonServiceProvider } from '@lara-node/horizon';
protected additionalProviders: ServiceProviderClass[] = [
// ...
HorizonServiceProvider,
];Publish and edit the config:
pnpm artisan vendor:publish --tag=config --provider=horizon// src/config/horizon.config.ts (after vendor:publish)
export default {
enabled: true,
path: '/horizon',
environments: {
production: {
supervisor: [
{ name: 'default', queue: 'default', processes: 3, tries: 3, memory: 128, timeout: 60 },
{ name: 'high', queue: 'high', processes: 5, tries: 3, memory: 256, timeout: 30 },
],
},
},
};The middleware registry and MiddlewareStack are stored on globalThis so they survive when multiple semver-compatible versions of @lara-node/core are loaded in the same process. This means registrations made in one module instance are always visible to resolution calls in another — no "Unknown middleware" errors from version drift.
globalThis.__lara_node_middleware_registry__ // alias map
globalThis.__lara_node_middleware_stack__ // MiddlewareStack singleton
# Clone the monorepo
git clone https://github.com/venomous-maker/vest.git
cd vest
pnpm install
pnpm -r build # build all packages
pnpm -r test # run all tests
pnpm -r typecheck # typecheck everything
# Publish a package (workspace:^ ensures correct semver ranges)
cd packages/core
pnpm publish --no-git-checksAll packages use workspace:^ for cross-package dependencies so published versions carry compatible semver ranges (^x.y.z) rather than exact pins. This guarantees a single shared instance of @lara-node/core in every consumer project.
MIT