| name | create-module |
| description | Create a new NestJS module with repository, service, controller, schema, and Drizzle table definition. Use when adding new feature modules, API endpoints, or business domains. |
| argument-hint | <module-name> |
| disable-model-invocation | true |
Create NestJS Module (PostgreSQL / Drizzle)
Create a new NestJS module for Mix Space project. Module name: $ARGUMENTS
Directory Structure
Create the following files under apps/core/src/modules/<module-name>/:
<module-name>/
├── <name>.module.ts # Module definition
├── <name>.controller.ts # HTTP controller
├── <name>.service.ts # Business logic
├── <name>.repository.ts # Drizzle repository (extends BaseRepository)
├── <name>.schema.ts # Zod validation schemas for API DTOs
├── <name>.types.ts # TypeScript row/input types
└── <name>.enum.ts # Enums (optional, only if needed)
Also add the Drizzle table definition in apps/core/src/database/schema/.
File Templates
0. Drizzle Table (database/schema/<name>.ts)
Create a new schema file (or append to an existing one) in apps/core/src/database/schema/.
import { index, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core'
import { createdAt, pkText, refText, tsCol, updatedAt } from './columns'
export const <name>s = pgTable(
'<name>s',
{
id: pkText(),
createdAt: createdAt(),
name: text('name').notNull(),
},
(table) => [
],
)
Then re-export it from apps/core/src/database/schema/index.ts:
export * from './<name>'
Column helpers (from columns.ts):
pkText() — Snowflake primary key as text (auto-named id)
refText(name) — Snowflake foreign-key reference as text
createdAt() — created_at timestamp with default now()
updatedAt() — updated_at nullable timestamp
tsCol(name) — generic timestamp column
Common column types:
text('col') — string
integer('col') — number
boolean('col') — boolean (use .default(false))
jsonb('col').$type<T>() — JSON data
text('col').array() — text array (e.g., tags)
1. Types (<name>.types.ts)
import type { EntityId } from '~/shared/id/entity-id'
export interface <Name>Row {
id: EntityId
name: string
createdAt: Date
}
export interface <Name>CreateInput {
name: string
}
export type <Name>PatchInput = Partial<<Name>CreateInput>
2. Repository (<name>.repository.ts)
import { Inject, Injectable } from '@nestjs/common'
import { desc, eq, sql } from 'drizzle-orm'
import { PG_DB_TOKEN } from '~/constants/system.constant'
import { <name>s } from '~/database/schema'
import {
BaseRepository,
type PaginationResult,
toEntityId,
} from '~/processors/database/base.repository'
import type { AppDatabase } from '~/processors/database/postgres.provider'
import { type EntityId, parseEntityId } from '~/shared/id/entity-id'
import { SnowflakeService } from '~/shared/id/snowflake.service'
import type { <Name>CreateInput, <Name>PatchInput, <Name>Row } from './<name>.types'
const mapRow = (row: typeof <name>s.$inferSelect): <Name> => ({
: (row.) ,
: row.,
: row.,
})
()
<> {
() {
(db)
}
(
page = ,
size = ,
?: <, >,
): <<<>>> {
page = .(, page)
size = .(, .(, size))
offset = (page - ) * size
[rows, [{ count }]] = .([
.
.()
.(<name>s)
.((<name>s.))
.(size)
.(offset),
..({ : sql<> }).(<name>s),
])
{
: rows.(mapRow),
: .((count ?? ), page, size),
}
}
(): <<>[]> {
rows = .
.()
.(<name>s)
.((<name>s.))
rows.(mapRow)
}
(: | ): <<> | > {
idBig = (id)
[row] = .
.()
.(<name>s)
.((<name>s., idBig))
.()
row ? (row) :
}
(: <>): <<>> {
id = ..()
[row] = .
.(<name>s)
.({
id,
: input.,
})
.()
(row)
}
(
: | ,
: <>,
): <<> | > {
idBig = (id)
: < <name>s.> = {}
(patch. !== ) update. = patch.
(.(update). === ) {
[existing] = .
.()
.(<name>s)
.((<name>s., idBig))
.()
existing ? (existing) :
}
[row] = .
.(<name>s)
.(update)
.((<name>s., idBig))
.()
row ? (row) :
}
(: | ): <<> | > {
idBig = (id)
[row] = .
.(<name>s)
.((<name>s., idBig))
.()
row ? (row) :
}
(): <> {
[row] = .
.({ : sql<> })
.(<name>s)
(row?. ?? )
}
}
Key patterns:
- Inject
PG_DB_TOKEN (the Drizzle AppDatabase instance) and SnowflakeService
- Use
parseEntityId(id) to validate incoming Snowflake IDs before queries
- Use
toEntityId(row.id) when mapping rows out of the repository
- Use
this.snowflake.nextId() to generate new Snowflake IDs on insert
- Use
.returning() on insert/update/delete to get the affected row back
3. Service (<name>.service.ts)
import { Injectable } from '@nestjs/common'
import { <Name>Repository } from './<name>.repository'
@Injectable()
export class <Name>Service {
constructor(private readonly <name>Repository: <Name>Repository) {}
public get repository() {
return this.<name>Repository
}
}
4. Schema / DTOs (<name>.schema.ts)
import { createZodDto } from 'nestjs-zod'
import { z } from 'zod'
import { zNonEmptyString } from '~/common/zod'
export const <Name>Schema = z.object({
name: zNonEmptyString,
})
export class <Name>Dto extends createZodDto(<Name>Schema) {}
export const Partial<Name>Schema = <Name>Schema.partial()
export class Partial<Name>Dto extends createZodDto(Partial<Name>Schema) {}
export type <Name>Input = z.infer<typeof <Name>Schema>
export <> = z.< <>>
ID validation: Use EntityIdDto from ~/shared/dto/id.dto for route params:
import { EntityIdDto } from '~/shared/dto/id.dto'
Common zod primitives (from ~/common/zod):
zNonEmptyString — z.string().min(1)
zCoerceBoolean — coerces string "true"/"1" to boolean
zCoerceInt / zCoercePositiveInt — coerced number validators
zPaginationPage / zPaginationSize — pagination defaults
zEntityId — validates Snowflake string format
zHttpsUrl — HTTPS URL validator
zEmail(msg) — email validator with message
5. Controller (<name>.controller.ts)
Option A: Manual controller (for custom routes and logic):
import { Body, Delete, Get, HttpCode, Param, Post, Put, Query } from '@nestjs/common'
import { ApiController } from '~/common/decorators/api-controller.decorator'
import { Auth } from '~/common/decorators/auth.decorator'
import { HTTPDecorators } from '~/common/decorators/http.decorator'
import { EntityIdDto } from '~/shared/dto/id.dto'
import { PagerDto } from '~/shared/dto/pager.dto'
import { <Name>Service } from './<name>.service'
import { <Name>Dto, Partial<Name>Dto } from './<name>.schema'
@ApiController('<name>s')
export class <Name>Controller {
constructor( <name>: <>) {}
()
() {
{ page, size } = query
.<name>..(page, size)
}
()
() {
.<name>..()
}
()
() {
.<name>..(params.)
}
()
()
.()
() {
.<name>..(body)
}
()
()
() {
.<name>..(params., body)
}
()
()
()
() {
.<name>..(params.)
}
}
Option B: Auto-CRUD via BasePgCrudFactory (for simple CRUD modules):
import { Get, Query } from '@nestjs/common'
import { BasePgCrudFactory } from '~/transformers/crud-factor.pg.transformer'
import { PagerDto } from '~/shared/dto/pager.dto'
import { <Name>Repository } from './<name>.repository'
export class <Name>Controller extends BasePgCrudFactory({
repository: <Name>Repository,
}) {
}
6. Module (<name>.module.ts)
import { Module } from '@nestjs/common'
import { <Name>Controller } from './<name>.controller'
import { <Name>Repository } from './<name>.repository'
import { <Name>Service } from './<name>.service'
@Module({
controllers: [<Name>Controller],
providers: [<Name>Service, <Name>Repository],
exports: [<Name>Service, <Name>Repository],
})
export class <Name>Module {}
If the module needs to be globally available (used by many other modules), add @Global():
import { Global, Module } from '@nestjs/common'
@Global()
@Module({ })
export class <Name>Module {}
Register Module
After creating files, register the module in apps/core/src/app.module.ts:
- Add import statement for
<Name>Module
- Add
<Name>Module to the imports array
Register Repository Token (if needed for cross-module DI)
If other modules need to inject the repository by token, add an entry in apps/core/src/processors/database/repository.tokens.ts:
export const POSTGRES_REPOSITORY_TOKENS = {
<name>: Symbol('<Name>Repository'),
} as const
Then provide it in the module:
{
provide: POSTGRES_REPOSITORY_TOKENS.<name>,
useExisting: <Name>Repository,
}
Generate Schema Migration
After adding the Drizzle table definition, generate a SQL migration:
pnpm drizzle-kit generate
This creates a new numbered SQL file in apps/core/src/database/migrations/.
Project Conventions
- Use
@ApiController() instead of @Controller() — adds /api/v2 prefix in production
- IDs are Snowflake strings (not MongoDB ObjectIds). Validate with
zEntityId / EntityIdDto
- Schema defined in
database/schema/ using Drizzle pgTable()
- Repositories extend
BaseRepository and inject PG_DB_TOKEN + SnowflakeService
- Use Zod schemas for request validation, not class-validator
- Use
@Auth() decorator for authenticated endpoints
- Use
@HTTPDecorators.Paginator or PagerDto for paginated endpoints
- Use
@HTTPDecorators.Idempotence() for POST endpoints to prevent duplicates
- Response keys are auto-converted to snake_case by
JSONTransformInterceptor