Skip to content

CLI Guide

Veloce-TS ships with a built-in CLI for project creation and code generation. Every generated file passes npm run typecheck out of the box, and since v3.1.0 everything the CLI produces runs on Node as well as Bun.

The veloce and veloce-ts binaries are available after installing the package:

Terminal window
npm install veloce-ts
# or globally
npm install -g veloce-ts
# Bun works the same way
bun add veloce-ts

Scaffolds a complete project.

Terminal window
veloce new my-api

Generated structure (the rest template; each one writes only the directories it fills):

my-api/
├── src/
│ ├── index.ts # app entry with usePlugin calls
│ ├── controllers/
│ │ └── user.controller.ts # injects UserService with @Inject
│ └── services/
│ └── user.service.ts # resolved through the DI container
├── public/
│ └── docs.html # standalone Swagger UI
├── package.json # scripts route through the veloce binary
├── tsconfig.json # Stage-2 decorators + reflect-metadata
├── .env.example # every variable the generated code reads
├── .gitignore
└── README.md

The graphql and fullstack templates add src/resolvers/, websocket and fullstack add src/websockets/, and all of them except websocket add src/services/.

Options:

OptionDefaultWhat it does
-t, --template <template>restrest, graphql, websocket or fullstack
--installoffInstall dependencies (Bun when available, npm otherwise)
--gitoffgit init plus an initial commit — skipped when the target is already inside a repository

The examples use the DI container. The generated controller and resolver ask for their UserService with @Inject(UserService) rather than constructing it, so replacing the in-memory array with a database leaves every caller untouched. Scaffolding that news up its dependencies teaches the opposite of what the framework provides.

Templates:

Terminal window
veloce new my-api # REST (default)
veloce new my-api --template rest
veloce new my-api --template graphql # GraphQL resolver + playground
veloce new my-api --template websocket # WebSocket gateway
veloce new my-api --template fullstack # REST + GraphQL + WebSocket

:::note All four templates run on Node Including websocket and fullstack, which used to throw at startup there. They declare @hono/node-ws (unused under Bun and Deno, which upgrade natively). CI scaffolds every template and installs, type-checks, builds and boots it under Node on each run. :::

The generated package.json routes its scripts through the veloce binary rather than hardcoding a runtime, so the same project works either way:

{
"scripts": {
"dev": "veloce dev",
"build": "veloce build",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
}
}

Starts the app with hot reload.

Terminal window
veloce dev
veloce dev --port 8080
veloce dev --runtime node # force the Node path

Under Bun it uses bun --watch. Under Node it runs tsx, which transpiles the Stage-2 decorators Node’s own type stripping cannot handle — tsx is included as a devDependency in generated projects.

OptionDefaultWhat it does
-p, --port <port>3000Port passed to the app as PORT
-w, --watch <path>srcExtra paths to watch
-r, --runtime <runtime>autoauto, bun or node

Compiles the project for production.

Terminal window
veloce build
veloce build --runtime node
veloce build --outdir build

Under Bun it bundles with Bun.build and emits ESM and CJS. Under Node it compiles with the project’s own tsc, honouring your tsconfig.json — which is what emits the decorator metadata the framework reads.

:::note The Bun path needs the CLI itself to be running under Bun Bun.build is an in-process API, so having the bun binary on PATH is not enough: npm run build on a machine with Bun installed still takes the Node path, because npm starts the CLI with Node. Use bun run build for the Bun path, or pass --runtime bun (which errors if the process is not Bun). :::

OptionDefaultWhat it does
-o, --outdir <dir>distOutput directory
-m, --minifyoffMinify (Bun path)
-s, --sourcemapoffEmit sourcemaps. Off by default: shipping them is a deployment-size decision, and the framework’s own package was 73% sourcemaps before that lesson landed in 3.1.2
-f, --format <format>bothesm, cjs or both — Bun path only
-r, --runtime <runtime>autoauto, bun or node

Generate individual pieces of your application. All commands accept a <name> argument.

Generates a full module folder: controller + service + DTO + barrel.

Terminal window
veloce generate module users

Output in src/modules/users/:

users.controller.ts # @Controller('/users') with CRUD stubs
users.service.ts # plain service class
users.dto.ts # CreateUsersDto, UpdateUsersDto (Zod)
users.module.ts # barrel: re-exports controller + service
Terminal window
veloce generate controller products
src/controllers/products.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body } from 'veloce-ts';
import { z } from 'zod';
const CreateProductsSchema = z.object({
name: z.string().min(1),
});
@Controller('/products')
export class ProductsController {
@Get('/')
async findAll() {
return [];
}
@Get('/:id')
async findOne(@Param('id') id: string) {
return { id };
}
@Post('/')
async create(@Body(CreateProductsSchema) body: z.infer<typeof CreateProductsSchema>) {
return body;
}
@Put('/:id')
async update(@Param('id') id: string, @Body() body: unknown) {
return { id, ...body as object };
}
@Delete('/:id')
async remove(@Param('id') id: string) {
return { id, deleted: true };
}
}
Terminal window
veloce generate service products
src/services/products.service.ts
export class ProductsService {
async findAll() {
return [];
}
async findOne(id: string) {
return { id };
}
async create(data: unknown) {
return data;
}
async update(id: string, data: unknown) {
return { id, ...data as object };
}
async remove(id: string) {
return { id, deleted: true };
}
}
Terminal window
veloce generate dto create-product
src/dtos/create-product.dto.ts
import { z } from 'zod';
export const CreateProductSchema = z.object({
name: z.string().min(1),
});
export type CreateProductDto = z.infer<typeof CreateProductSchema>;
Terminal window
veloce generate resolver user
src/resolvers/user.resolver.ts
import { Resolver, GQLQuery, GQLMutation, Arg } from 'veloce-ts/graphql';
import { z } from 'zod';
@Resolver('user')
export class UserResolver {
@GQLQuery('getUsers')
async getAll() {
return [];
}
@GQLQuery('getUser')
async getOne(@Arg('id', z.string()) id: string) {
return { id };
}
@GQLMutation('createUser')
async create(@Arg('name', z.string()) name: string) {
return { id: Date.now().toString(), name };
}
}
Terminal window
veloce generate middleware auth
src/middleware/auth.middleware.ts
import type { Context, Next } from 'hono';
export async function authMiddleware(c: Context, next: Next) {
const token = c.req.header('authorization');
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
}
await next();
}

A @WebSocket gateway with the three lifecycle hooks and a Zod message schema.

Terminal window
veloce generate gateway presence

Creates src/websockets/presence.websocket.ts with @OnConnect, @OnMessage and @OnDisconnect, joining a room named after the gateway. Register it like a controller:

app.include(PresenceGateway);
app.usePlugin(new WebSocketPlugin());

Only one @OnMessage per gateway — the metadata stores a single message handler — so model different client payloads as a discriminated union on type.

An @On event-listener class.

Terminal window
veloce generate listener audit

Creates src/listeners/audit.listener.ts. Listeners are registered with app.include() and resolved through the DI container, so they inject services exactly like a controller. Subscriptions attach at compile() and are removed at shutdown().

Terminal window
veloce generate plugin analytics
src/plugins/analytics.plugin.ts
import type { Plugin, VeloceTS } from 'veloce-ts';
export class AnalyticsPlugin implements Plugin {
name = 'analytics';
async register(app: VeloceTS): Promise<void> {
// register routes or middleware here
}
}
CommandOutput path
generate module <name>src/modules/<name>/
generate controller <name>src/controllers/<name>.controller.ts
generate service <name>src/services/<name>.service.ts
generate dto <name>src/dto/<name>.dto.ts
generate resolver <name>src/resolvers/<name>.resolver.ts
generate middleware <name>src/middleware/<name>.middleware.ts
generate plugin <name>src/plugins/<name>.plugin.ts
generate gateway <name>src/websockets/<name>.websocket.ts
generate listener <name>src/listeners/<name>.listener.ts

The CLI detects an existing src/ directory and places files there. If src/ is not found, files are placed in the current directory.

The <name> argument is normalized automatically:

InputPascalCasekebab-case
userUseruser
UserProfileUserProfileuser-profile
create-productCreateProductcreate-product