AI Unified Process for the NestJS/Drizzle + Next.js stack - migrations, implementation, tests
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Two use cases on one Product feature, end to end: UC-010 lists the available products (a read),
UC-011 creates a product (a write). Every relative import ends in .js because the layout
detection found NodeNext; drop the suffix where it did not.
// src/products/products.repository.ts
import { Inject, Injectable } from '@nestjs/common';
import { and, eq } from 'drizzle-orm';
import { DRIZZLE, type DrizzleDb } from '../database/drizzle.provider.js';
import { products } from '../database/schema.js';
@Injectable()
export class ProductsRepository {
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDb) {}
// UC-010 BR-010: Out-of-stock products are excluded regardless of any category filter.
async findAvailable(category?: string) {
const filters = [eq(products.inStock, true)];
if (category) filters.push(eq(products.category, category));
return this.db.select().from(products).where(and(...filters));
}
}// src/products/dto/product.response.ts
export type ProductResponse = {
id: number;
name: string;
category: string;
price: number;
};// src/products/products.service.ts
import { Injectable } from '@nestjs/common';
import { ProductsRepository } from './products.repository.js';
import type { ProductResponse } from './dto/product.response.js';
@Injectable()
export class ProductsService {
constructor(private readonly repository: ProductsRepository) {}
async listAvailable(category?: string): Promise<ProductResponse[]> {
const rows = await this.repository.findAvailable(category);
return rows.map((row) => ({
id: row.id,
name: row.name,
category: row.category,
price: row.price,
}));
}
}// src/products/dto/list-products.query.ts
import { IsOptional, IsString } from 'class-validator';
export class ListProductsQuery {
@IsOptional()
@IsString()
category?: string;
}// src/products/products.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { ListProductsQuery } from './dto/list-products.query.js';
import { ProductsService } from './products.service.js';
import type { ProductResponse } from './dto/product.response.js';
@Controller('products')
export class ProductsController {
constructor(private readonly service: ProductsService) {}
@Get()
async list(@Query() query: ListProductsQuery): Promise<ProductResponse[]> {
return this.service.listAvailable(query.category);
}
}// src/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller.js';
import { ProductsRepository } from './products.repository.js';
import { ProductsService } from './products.service.js';
@Module({
controllers: [ProductsController],
providers: [ProductsService, ProductsRepository],
})
export class ProductsModule {}Register the module in the application's root module.
A body DTO, a domain error, and a repository insert. The DTO's decorators carry the rules the
global ValidationPipe enforces; the rule that needs the database lives in the service.
// src/products/dto/create-product.body.ts
import { IsInt, IsNotEmpty, IsString, MaxLength, Min } from 'class-validator';
export class CreateProductBody {
@IsString()
@IsNotEmpty()
@MaxLength(100)
name!: string;
@IsString()
category!: string;
// UC-011 BR-001: The price is a whole number of cents and never negative.
@IsInt()
@Min(0)
price!: number;
}// src/products/products.repository.ts — added to the class above
async findByName(name: string) {
return (await this.db.select().from(products).where(eq(products.name, name)).limit(1))[0];
}
async insert(values: typeof products.$inferInsert) {
const [row] = await this.db.insert(products).values(values).returning();
return row;
}// src/products/products.service.ts — added to the class above
import { DuplicateNameError } from '../common/errors.js'; // the project's domain error
import type { CreateProductBody } from './dto/create-product.body.js';
async create(body: CreateProductBody): Promise<ProductResponse> {
// UC-011 BR-002: Product names are unique.
if (await this.repository.findByName(body.name)) {
throw new DuplicateNameError('A product with this name already exists.');
}
const row = await this.repository.insert({ ...body, inStock: true });
return { id: row.id, name: row.name, category: row.category, price: row.price };
}// src/products/products.controller.ts — added to the class above
import { Body, HttpCode, Post } from '@nestjs/common';
import { CreateProductBody } from './dto/create-product.body.js';
@Post()
@HttpCode(201)
async create(@Body() body: CreateProductBody): Promise<ProductResponse> {
return this.service.create(body);
}The message is the one the use case's alternative flow states. The project's exception filter maps
DuplicateNameError to its status code (typically 409) and error body; when the project has no
domain errors yet, throw Nest's ConflictException with the same message instead of inventing a
hierarchy.
Where the detection found no route indirection, the route file holds the page:
// src/app/products/page.tsx
'use client';
import { useEffect, useState } from 'react';
type Product = { id: number; name: string; category: string; price: number };
export default function ProductsPage() {
const [products, setProducts] = useState<Product[]>([]);
const [category, setCategory] = useState('');
useEffect(() => {
const query = category ? `?category=${encodeURIComponent(category)}` : '';
fetch(`/api/products${query}`)
.then((res) => res.json())
.then(setProducts);
}, [category]);
return (
<main>
<h1>Products</h1>
<label htmlFor="category">Category</label>
<select id="category" value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="">All</option>
<option value="tools">Tools</option>
</select>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} — {product.price}
</li>
))}
</ul>
</main>
);
}Where the detection found route indirection, the route file is a thin wrapper and the body above lives in the view component instead:
// src/app/products/page.tsx
'use client';
import { ProductsPage } from '../../views/ProductsPage';
export default function Page() {
return <ProductsPage />;
}