create-module
Generates the complete file structure and template code for a new NestJS module using Drizzle and PostgreSQL.
Install
mkdir -p .claude/skills/create-module && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3592" && unzip -o skill.zip -d .claude/skills/create-module && rm skill.zipInstalls to .claude/skills/create-module
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.Key capabilities
- →Scaffold NestJS module architecture
- →Generate Drizzle table definitions
- →Create repository, service, and controller files
- →Define Zod validation schemas for DTOs
- →Generate SQL migrations via drizzle-kit
How it works
The skill generates a set of predefined files including controllers, services, and repositories, while providing templates for Drizzle schema definitions and Zod validation.
Inputs & outputs
When to use create-module
- →Adding a new feature module to the project
- →Setting up a new API endpoint with database access
- →Defining a new database table and service
About this skill
Create NestJS Module (PostgreSQL / Drizzle)
Create a new NestJS module for MX 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(),
// Add other columns...
},
(table) => [
// Add indexes and unique constraints
// uniqueIndex('<name>s_name_uniq').on(table.name),
// index('<name>s_created_at_idx').on(table.createdAt),
],
)
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-namedid)refText(name)— Snowflake foreign-key reference as textcreatedAt()—created_attimestamp with defaultnow()updatedAt()—updated_atnullable timestamptsCol(name)— generic timestamp column
Common column types:
text('col')— stringinteger('col')— numberboolean('col')— boolean (use.default(false))jsonb('col').$type<T>()— JSON datatext('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 => ({
id: toEntityId(row.id) as EntityId,
name: row.name,
createdAt: row.createdAt,
})
@Injectable()
export class <Name>Repository extends BaseRepository {
constructor(
@Inject(PG_DB_TOKEN) db: AppDatabase,
private readonly snowflake: SnowflakeService,
) {
super(db)
}
async list(
page = 1,
size = 10,
filter?: Record<string, unknown>,
): Promise<PaginationResult<<Name>Row>> {
page = Math.max(1, page)
size = Math.min(50, Math.max(1, size))
const offset = (page - 1) * size
const [rows, [{ count }]] = await Promise.all([
this.db
.select()
.from(<name>s)
.orderBy(desc(<name>s.createdAt))
.limit(size)
.offset(offset),
this.db.select({ count: sql<number>`count(*)::int` }).from(<name>s),
])
return {
data: rows.map(mapRow),
pagination: this.paginationOf(Number(count ?? 0), page, size),
}
}
async findAll(): Promise<<Name>Row[]> {
const rows = await this.db
.select()
.from(<name>s)
.orderBy(desc(<name>s.createdAt))
return rows.map(mapRow)
}
async findById(id: EntityId | string): Promise<<Name>Row | null> {
const idBig = parseEntityId(id)
const [row] = await this.db
.select()
.from(<name>s)
.where(eq(<name>s.id, idBig))
.limit(1)
return row ? mapRow(row) : null
}
async create(input: <Name>CreateInput): Promise<<Name>Row> {
const id = this.snowflake.nextId()
const [row] = await this.db
.insert(<name>s)
.values({
id,
name: input.name,
})
.returning()
return mapRow(row)
}
async update(
id: EntityId | string,
patch: <Name>PatchInput,
): Promise<<Name>Row | null> {
const idBig = parseEntityId(id)
const update: Partial<typeof <name>s.$inferInsert> = {}
if (patch.name !== undefined) update.name = patch.name
if (Object.keys(update).length === 0) {
const [existing] = await this.db
.select()
.from(<name>s)
.where(eq(<name>s.id, idBig))
.limit(1)
return existing ? mapRow(existing) : null
}
const [row] = await this.db
.update(<name>s)
.set(update)
.where(eq(<name>s.id, idBig))
.returning()
return row ? mapRow(row) : null
}
async deleteById(id: EntityId | string): Promise<<Name>Row | null> {
const idBig = parseEntityId(id)
const [row] = await this.db
.delete(<name>s)
.where(eq(<name>s.id, idBig))
.returning()
return row ? mapRow(row) : null
}
async count(): Promise<number> {
const [row] = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(<name>s)
return Number(row?.count ?? 0)
}
}
Key patterns:
- Inject
PG_DB_TOKEN(the DrizzleAppDatabaseinstance) andSnowflakeService - 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
}
// Add business logic methods here.
// Simple CRUD is delegated to the repository.
// Cross-module orchestration, validation, and events go in the service.
}
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,
// Add other fields...
})
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) {}
// Type exports
export type <Name>Input = z.infer<typeof <Name>Schema>
export type Partial<Name>Input = z.infer<typeof Partial<Name>Schema>
ID validation: Use EntityIdDto from ~/shared/dto/id.dto for route params:
import { EntityIdDto } from '~/shared/dto/id.dto'
// In controller: @Param() params: EntityIdDto
// Access: params.id (validated Snowflake string)
Common zod primitives (from ~/common/zod):
zNonEmptyString—z.string().min(1)zCoerceBoolean— coerces string"true"/"1"to booleanzCoerceInt/zCoercePositiveInt— coerced number validatorszPaginationPage/zPaginationSize— pagination defaultszEntityId— validates Snowflake string formatzHttpsUrl— HTTPS URL validatorzEmail(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(private readonly <name>Service: <Name>Service) {}
@Get('/')
async getPaginate(@Query() query: PagerDto) {
const { page, size } = query
return this.<name>Service.repository.list(page, size)
}
@Get('/all')
async getAll() {
return this.<name>Service.repository.findAll()
}
@Get('/:id')
async getById(@Param() params: EntityIdDto) {
return this.<name>Service.repository.findById(params.id)
}
@Post('/')
@Auth()
@HTTPDecorators.Idempotence()
async create(@Body() body: <Name>Dto) {
return this.<name>Service.repository.create(body)
}
@Put('/:id')
@Auth()
async update(@Param() params: EntityIdDto, @Body() body: <Name>Dto) {
return this.<name>Service.repository.update(params.id, body)
}
@Delete('/:id')
@Auth()
@HttpCode(204)
async delete(@Param() params: EntityIdDto) {
await this.<name>Service.repository.deleteById(params.id)
}
}
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>Re
---
*Content truncated.*
When not to use it
- →Adding files to non-NestJS projects
- →Creating modules that do not use PostgreSQL
Prerequisites
Limitations
- →Requires manual registration of the module in app.module.ts
- →Requires manual SQL migration generation
How it compares
It enforces project-specific architectural patterns and naming conventions automatically, rather than manually creating and linking each file.
Compared to similar skills
create-module side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| create-module (this skill) | 1 | 3mo | Review | Intermediate |
| supabase-mcp-integration | 13 | 8mo | Review | Advanced |
| alwib-backend | 0 | 5mo | No flags | Advanced |
| supabase-migration-deep-dive | 1 | 26d | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by mx-space
View all by mx-space →You might also like
supabase-mcp-integration
manutej
Comprehensive Supabase integration covering authentication, database operations, realtime subscriptions, storage, and MCP server patterns for building production-ready backends with PostgreSQL, Auth, and real-time capabilities
alwib-backend
Tsuev
Develop and maintain the Alwib NestJS backend in `backend/`. Use when implementing or refactoring modules, controllers, services, DTO validation, Swagger docs, authentication/authorization flows, Prisma schema and migrations, and backend lint/test flows.
supabase-migration-deep-dive
jeremylongshore
Execute Supabase major re-architecture and migration strategies with strangler fig pattern. Use when migrating to or from Supabase, performing major version upgrades, or re-platforming existing integrations to Supabase. Trigger with phrases like "migrate supabase", "supabase migration", "switch to supabase", "supabase replatform", "supabase upgrade major".
azure-postgres-ts
aiskillstore
Connect to Azure Database for PostgreSQL Flexible Server from Node.js/TypeScript using the pg (node-postgres) package.
drizzle
lobehub
Drizzle ORM schema and database guide. Use when working with database schemas (src/database/schemas/*), defining tables, creating migrations, or database model code. Triggers on Drizzle schema definition, database migrations, or ORM usage questions.
supabase-developer
daffy0208
Build full-stack applications with Supabase (PostgreSQL, Auth, Storage, Real-time, Edge Functions). Use when implementing authentication, database design with RLS, file storage, real-time features, or serverless functions.