원클릭으로
api-conventions
// MX Space API design conventions. Apply when writing controllers, API endpoints, or handling HTTP requests.
// MX Space API design conventions. Apply when writing controllers, API endpoints, or handling HTTP requests.
Use when releasing mx-core server (apps/core), @mx-space/api-client, or @mx-space/cli (mxs) — version bump, changelog, git tag, Docker build, GitHub Release, and Dokploy redeploy. Triggers on "发版", "release a new version", "cut a release", "bump version", "publish api-client", "publish cli", "release mxs".
Use when an agent must inspect, draft, validate, edit, publish, unpublish, delete, or configure mx-space content through packages/cli or the mxs binary.
Author and review Drizzle SQL migrations safely for rolling deploys. Triggers when editing apps/core/src/database/schema/*.ts or apps/core/src/database/migrations/*.sql, when the user runs drizzle-kit generate, when "lint-migrations" reports a violation, or on prompts like "迁移", "改 schema", "alter table", "add a column", "drop column", "migration safety". Enforces the expand-contract pattern because mx-core ships rolling deploys (Dokploy, 2 replicas) where new and old pods coexist for tens of seconds during cutover.
Create E2E test file for a specified module. Use when adding end-to-end tests for controllers or unit tests for services and repositories.
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.
Review code for MX Space project conventions. Checks NestJS patterns, Drizzle ORM repositories, Zod schemas, API design, etc.
| name | api-conventions |
| description | MX Space API design conventions. Apply when writing controllers, API endpoints, or handling HTTP requests. |
| user-invocable | false |
// Use @ApiController instead of @Controller
// Dev environment has no prefix, production auto-adds /api/v{version} prefix
@ApiController('posts') // ✓
@Controller('posts') // ✗
// Endpoints requiring login
@Auth()
async create() {}
// Optional auth (get current user status)
async get(@IsAuthenticated() isAuth: boolean) {}
// Get current user
async get(@CurrentUser() user: UserModel) {}
ResponseInterceptor automatically handles response format:
| Return Type | Transformed Result |
|---|---|
Array | { data: [...] } |
Object | Returned directly |
undefined | 204 No Content |
@Bypass | Returned as-is, skips transformation |
JSONTransformInterceptor converts all fields to snake_case:
createdAt → created_atcategoryId → category_id// Controller: return PaginationResult<T> from the repository
@Get('/')
async list(@Query() query: PagerDto) {
return this.postRepository.list({
page: query.page,
size: query.size,
sortBy: query.sortBy,
sortOrder: query.sortOrder,
})
}
// Repository returns { data: T[], pagination: {...} } directly
// The ResponseInterceptor auto-wraps this as { data: [...], pagination: {...} }
For CRUD boilerplate, use BasePgCrudFactory:
@ApiController(paths)
export class LinkControllerCrud extends BasePgCrudFactory({
repository: LinkRepository,
}) {
@Get('/')
async gets(@Query() pager: PagerDto) {
const { size = 10, page = 1 } = pager
return this.repository.list(page, size)
}
}
// Path parameters — use EntityIdDto for Snowflake entity IDs
@Get('/:id')
async get(@Param() params: EntityIdDto) {
return this.service.findById(params.id)
}
// For integer IDs or entity IDs (e.g. notes with nid)
@Get('/:id')
async get(@Param() params: IntIdOrEntityIdDto) {}
// Query parameters
@Get('/')
async list(@Query() query: PagerDto) {}
// Request body
@Post('/')
async create(@Body() body: CreateDto) {}
| Method | Purpose | Status Code |
|---|---|---|
| GET | Retrieve resource | 200 |
| POST | Create resource | 201 |
| PUT | Full update | 200 |
| PATCH | Partial update | 200 |
| DELETE | Delete resource | 204 |
import { BusinessException } from '~/common/exceptions/biz.exception'
import { ErrorCodeEnum } from '~/constants/error-code.constant'
// Business errors
throw new BusinessException(ErrorCodeEnum.PostNotFound)
throw new BusinessException(ErrorCodeEnum.SlugNotAvailable, slug)
// HTTP errors
throw new BadRequestException('Invalid input')
throw new NotFoundException('Resource not found')
throw new UnauthorizedException('Not logged in')
// Add idempotency protection for create operations
@Post('/')
@HTTPDecorators.Idempotence()
async create() {}
// Custom idempotency key
@HTTPDecorators.Idempotence({ key: 'custom-key' })
// Disable cache
@Get('/')
@HttpCache.disable
async list() {}
// Custom cache
@HttpCache({ ttl: 60, key: 'my-key' })
async get() {}