260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
+87 -182
View File
@@ -67,9 +67,10 @@ Comprehensive best practices and architecture guide for NestJS applications, des
- 9.2 [Use Message and Event Patterns Correctly](#92-use-message-and-event-patterns-correctly)
- 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs)
10. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM**
- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)
- 10.3 [Use Structured Logging](#103-use-structured-logging)
- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)
- 10.3 [Use Structured Logging](#103-use-structured-logging)
---
@@ -390,7 +391,7 @@ export class UserAndOrderService {
private userRepo: UserRepository,
private orderRepo: OrderRepository,
private mailer: MailService,
private payment: PaymentService,
private payment: PaymentService
) {}
async createUser(dto: CreateUserDto) {
@@ -461,7 +462,7 @@ export class OrdersController {
constructor(
private orders: OrdersService,
private payment: PaymentService,
private notifications: NotificationService,
private notifications: NotificationService
) {}
@Post()
@@ -495,7 +496,7 @@ export class OrdersService {
private emailService: EmailService,
private analyticsService: AnalyticsService,
private notificationService: NotificationService,
private loyaltyService: LoyaltyService,
private loyaltyService: LoyaltyService
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -526,7 +527,7 @@ export class OrderCreatedEvent {
public readonly orderId: string,
public readonly userId: string,
public readonly items: OrderItem[],
public readonly total: number,
public readonly total: number
) {}
}
@@ -535,17 +536,14 @@ export class OrderCreatedEvent {
export class OrdersService {
constructor(
private eventEmitter: EventEmitter2,
private repo: Repository<Order>,
private repo: Repository<Order>
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Emit event - no knowledge of consumers
this.eventEmitter.emit(
'order.created',
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
);
this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));
return order;
}
@@ -596,9 +594,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
// Complex queries in services
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic
@@ -623,9 +619,7 @@ export class UsersService {
// Custom repository with encapsulated queries
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } });
@@ -735,7 +729,7 @@ export class OrdersService {
constructor(
private usersService: UsersService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
private paymentService: PaymentService
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -810,14 +804,14 @@ interface NotificationService {
@Injectable()
export class OrdersService {
constructor(
private notifications: NotificationService, // Depends on 8 methods, uses 1
private notifications: NotificationService // Depends on 8 methods, uses 1
) {}
async confirmOrder(order: Order): Promise<void> {
await this.notifications.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
`Your order ${order.id} has been confirmed.`
);
}
}
@@ -825,12 +819,12 @@ export class OrdersService {
// Testing is painful - must mock unused methods
const mockNotificationService = {
sendEmail: jest.fn(),
sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required
sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required
getDeliveryStatus: jest.fn(), // Never used, but required
retryFailed: jest.fn(), // Never used, but required
retryFailed: jest.fn(), // Never used, but required
scheduleNotification: jest.fn(), // Never used, but required
};
```
@@ -887,14 +881,14 @@ export class SendGridEmailService implements EmailSender {
@Injectable()
export class OrdersService {
constructor(
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency
@Inject(EMAIL_SENDER) private emailSender: EmailSender // Minimal dependency
) {}
async confirmOrder(order: Order): Promise<void> {
await this.emailSender.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
`Your order ${order.id} has been confirmed.`
);
}
}
@@ -932,7 +926,7 @@ type MultiChannelSender = EmailSender & SmsSender & PushSender;
export class AlertService {
constructor(
@Inject(MULTI_CHANNEL_SENDER)
private sender: EmailSender & SmsSender,
private sender: EmailSender & SmsSender
) {}
async sendCriticalAlert(user: User, message: string): Promise<void> {
@@ -1123,9 +1117,7 @@ export class OrdersService {
```typescript
// Shared test suite that any implementation must pass
function testPaymentGatewayContract(
createGateway: () => PaymentGateway,
) {
function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
describe('PaymentGateway contract', () => {
let gateway: PaymentGateway;
@@ -1142,13 +1134,11 @@ function testPaymentGatewayContract(
});
it('throws InvalidCurrencyException for unsupported currency', async () => {
await expect(gateway.charge(1000, 'INVALID'))
.rejects.toThrow(InvalidCurrencyException);
await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
});
it('throws TransactionNotFoundException for invalid refund', async () => {
await expect(gateway.refund('nonexistent'))
.rejects.toThrow(TransactionNotFoundException);
await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
});
});
}
@@ -1204,7 +1194,7 @@ export class UsersService {
export class UsersService {
constructor(
private readonly userRepo: UserRepository,
@Inject('CONFIG') private readonly config: ConfigType,
@Inject('CONFIG') private readonly config: ConfigType
) {}
async findAll(): Promise<User[]> {
@@ -1359,7 +1349,9 @@ interface PaymentGateway {
@Injectable()
export class StripeService implements PaymentGateway {
charge(amount: number) { /* ... */ }
charge(amount: number) {
/* ... */
}
}
@Injectable()
@@ -1398,9 +1390,7 @@ export class MockPaymentService implements PaymentGateway {
providers: [
{
provide: PAYMENT_GATEWAY,
useClass: process.env.NODE_ENV === 'test'
? MockPaymentService
: StripeService,
useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,
},
],
exports: [PAYMENT_GATEWAY],
@@ -1410,9 +1400,7 @@ export class PaymentModule {}
// Injection
@Injectable()
export class OrdersService {
constructor(
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
) {}
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
async createOrder(dto: CreateOrderDto) {
await this.payment.charge(dto.amount);
@@ -1654,7 +1642,7 @@ export class UsersController {
export class EntityNotFoundException extends Error {
constructor(
public readonly entity: string,
public readonly id: string,
public readonly id: string
) {
super(`${entity} with ID "${id}" not found`);
}
@@ -1773,20 +1761,11 @@ export class AllExceptionsFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
const message = exception instanceof HttpException ? exception.message : 'Internal server error';
this.logger.error(
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : exception,
);
this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);
response.status(status).json({
statusCode: status,
@@ -1798,10 +1777,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
}
// Register globally in main.ts
app.useGlobalFilters(
new AllExceptionsFilter(app.get(Logger)),
new DomainExceptionFilter(),
);
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());
// Or via module
@Module({
@@ -1931,7 +1907,7 @@ export class AuthService {
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private config: ConfigService,
private usersService: UsersService,
private usersService: UsersService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
@@ -2271,15 +2247,12 @@ export class AdminController {
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
private reflector: Reflector
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check for @Public() decorator
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
@@ -2309,10 +2282,7 @@ export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);
if (!requiredRoles) return true;
@@ -2387,9 +2357,9 @@ export class UsersController {
// DTOs without validation decorators
export class CreateUserDto {
name: string; // No validation
email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999
name: string; // No validation
email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999
}
```
@@ -2402,13 +2372,13 @@ async function bootstrap() {
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types
transformOptions: {
enableImplicitConversion: true,
},
}),
})
);
await app.listen(3000);
@@ -2573,7 +2543,7 @@ export class DatabaseService implements OnModuleInit {
export class CacheWarmerService implements OnApplicationBootstrap {
constructor(
private cache: CacheService,
private products: ProductsService,
private products: ProductsService
) {}
async onApplicationBootstrap(): Promise<void> {
@@ -2697,10 +2667,7 @@ export class ModuleLoaderService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async load<T>(
key: string,
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
): Promise<ModuleRef> {
async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {
if (!this.loadedModules.has(key)) {
const module = await importFn();
const moduleType = 'default' in module ? module.default : module;
@@ -2915,9 +2882,7 @@ export class UsersService {
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
stores: [
new KeyvRedis(config.get('REDIS_URL')),
],
stores: [new KeyvRedis(config.get('REDIS_URL'))],
ttl: 60 * 1000, // Default 60s
}),
}),
@@ -2930,7 +2895,7 @@ export class AppModule {}
export class ProductsService {
constructor(
@Inject(CACHE_MANAGER) private cache: Cache,
private productsRepo: ProductRepository,
private productsRepo: ProductRepository
) {}
async getPopular(): Promise<Product[]> {
@@ -2981,10 +2946,7 @@ export class CacheInvalidationService {
@OnEvent('product.updated')
@OnEvent('product.deleted')
async invalidateProductCaches(event: ProductEvent) {
await Promise.all([
this.cache.del('products:popular'),
this.cache.del(`product:${event.productId}`),
]);
await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);
}
}
```
@@ -3055,7 +3017,7 @@ describe('UsersController (e2e)', () => {
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
})
);
await app.init();
@@ -3091,9 +3053,7 @@ describe('UsersController (e2e)', () => {
describe('/users/:id (GET)', () => {
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/non-existent-id')
.expect(404);
return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);
});
});
});
@@ -3121,9 +3081,7 @@ describe('Protected Routes (e2e)', () => {
});
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/users/me')
.expect(401);
return request(app.getHttpServer()).get('/users/me').expect(401);
});
it('should return user profile with valid token', () => {
@@ -3254,9 +3212,7 @@ describe('WeatherService', () => {
});
it('should handle API timeout', async () => {
httpService.get.mockReturnValue(
throwError(() => new Error('ETIMEDOUT')),
);
httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
});
@@ -3265,7 +3221,7 @@ describe('WeatherService', () => {
httpService.get.mockReturnValue(
throwError(() => ({
response: { status: 429, data: { message: 'Rate limited' } },
})),
}))
);
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
@@ -3287,10 +3243,7 @@ describe('UsersService', () => {
};
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],
}).compile();
service = module.get(UsersService);
@@ -3433,9 +3386,7 @@ describe('UsersService', () => {
it('should throw on duplicate email', async () => {
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
await expect(
service.create({ name: 'Test', email: 'test@test.com' }),
).rejects.toThrow(ConflictException);
await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);
});
});
@@ -3813,12 +3764,7 @@ export class OrdersService {
for (const item of items) {
await manager.save(OrderItem, { orderId: order.id, ...item });
await manager.decrement(
Inventory,
{ productId: item.productId },
'stock',
item.quantity,
);
await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
}
// If this throws, everything rolls back
@@ -3841,12 +3787,7 @@ export class TransferService {
try {
// Debit source account
await queryRunner.manager.decrement(
Account,
{ id: fromId },
'balance',
amount,
);
await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
// Verify sufficient funds
const source = await queryRunner.manager.findOne(Account, {
@@ -3857,12 +3798,7 @@ export class TransferService {
}
// Credit destination account
await queryRunner.manager.increment(
Account,
{ id: toId },
'balance',
amount,
);
await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
// Log the transaction
await queryRunner.manager.save(TransactionLog, {
@@ -3887,13 +3823,10 @@ export class TransferService {
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
private dataSource: DataSource,
private dataSource: DataSource
) {}
async createWithProfile(
userData: CreateUserDto,
profileData: CreateProfileDto,
): Promise<User> {
async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
return this.dataSource.transaction(async (manager) => {
const user = await manager.save(User, userData);
await manager.save(Profile, { ...profileData, userId: user.id });
@@ -4034,7 +3967,7 @@ export class UsersController {
@SerializeOptions({ type: UserResponseDto })
async findAll(): Promise<UserResponseDto[]> {
const users = await this.usersService.findAll();
return users.map(u => plainToInstance(UserResponseDto, u));
return users.map((u) => plainToInstance(UserResponseDto, u));
}
@Get(':id')
@@ -4650,10 +4583,7 @@ export class UsersService {
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(
@Param('id') id: string,
@Headers('X-API-Version') version: string = '1',
): Promise<any> {
async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {
return this.usersService.findOne(id, version);
}
}
@@ -5137,11 +5067,7 @@ import { BullModule } from '@nestjs/bullmq';
},
},
}),
BullModule.registerQueue(
{ name: 'email' },
{ name: 'reports' },
{ name: 'notifications' },
),
BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),
],
})
export class QueueModule {}
@@ -5149,9 +5075,7 @@ export class QueueModule {}
// Producer: Add jobs to queue
@Injectable()
export class ReportsService {
constructor(
@InjectQueue('reports') private reportsQueue: Queue,
) {}
constructor(@InjectQueue('reports') private reportsQueue: Queue) {}
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
// Return immediately, process in background
@@ -5249,7 +5173,7 @@ export class NotificationService {
{
attempts: 5,
backoff: { type: 'exponential', delay: 5000 },
},
}
);
}
}
@@ -5267,7 +5191,7 @@ export class ScheduledJobsService implements OnModuleInit {
{
repeat: { cron: '0 0 * * *' },
jobId: 'daily-cleanup', // Prevent duplicates
},
}
);
// Send digest every hour
@@ -5277,7 +5201,7 @@ export class ScheduledJobsService implements OnModuleInit {
{
repeat: { every: 60 * 60 * 1000 },
jobId: 'hourly-digest',
},
}
);
}
}
@@ -5406,9 +5330,7 @@ export class DatabaseService implements OnApplicationShutdown {
console.log(`Database service shutting down on ${signal}`);
// Close all connections gracefully
await Promise.all(
this.connections.map((conn) => conn.close()),
);
await Promise.all(this.connections.map((conn) => conn.close()));
console.log('All database connections closed');
}
@@ -5477,9 +5399,7 @@ export class HealthController {
throw new ServiceUnavailableException('Shutting down');
}
return this.health.check([
() => this.db.pingCheck('database'),
]);
return this.health.check([() => this.db.pingCheck('database')]);
}
}
@@ -5535,10 +5455,7 @@ export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
});
// Wait with timeout
await Promise.race([
this.shutdownPromise,
new Promise((resolve) => setTimeout(resolve, 30000)),
]);
await Promise.race([this.shutdownPromise, new Promise((resolve) => setTimeout(resolve, 30000))]);
}
console.log('All requests completed');
@@ -5608,9 +5525,7 @@ export const appConfig = registerAs('app', () => ({
// config/validation.schema.ts
export const validationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432),
@@ -5684,7 +5599,7 @@ export class AppService {
export class DatabaseService {
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
private dbConfig: ConfigType<typeof databaseConfig>
) {
// Full type inference!
const host = this.dbConfig.host; // string
@@ -5694,12 +5609,7 @@ export class DatabaseService {
// Environment files support
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env.local',
'.env',
],
envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
});
// .env.development
@@ -5757,9 +5667,7 @@ logger.log('User ' + userId + ' created at ' + new Date());
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger:
process.env.NODE_ENV === 'production'
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'],
process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],
});
}
@@ -5794,7 +5702,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(),
message,
...context,
}),
})
);
}
@@ -5806,7 +5714,7 @@ export class JsonLogger implements LoggerService {
message,
trace,
...context,
}),
})
);
}
@@ -5817,7 +5725,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(),
message,
...context,
}),
})
);
}
@@ -5828,7 +5736,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(),
message,
...context,
}),
})
);
}
}
@@ -5878,7 +5786,7 @@ export class ContextLogger {
userId: this.cls.get('userId'),
message,
...data,
}),
})
);
}
@@ -5893,7 +5801,7 @@ export class ContextLogger {
error: error.message,
stack: error.stack,
...data,
}),
})
);
}
}
@@ -5906,10 +5814,7 @@ import { LoggerModule } from 'nestjs-pino';
LoggerModule.forRoot({
pinoHttp: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
redact: ['req.headers.authorization', 'req.body.password'],
serializers: {
req: (req) => ({
@@ -5955,4 +5860,4 @@ Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)
---
*Generated by build-agents.ts on 2026-01-16*
_Generated by build-agents.ts on 2026-01-16_
+11 -11
View File
@@ -36,11 +36,12 @@ npx skills add Kadajett/agent-nestjs-skills -a claude-code -a cursor
- `area-description.md` - Individual rule files
- `scripts/` - Build scripts and utilities
- `metadata.json` - Document metadata (version, organization, abstract)
- __`AGENTS.md`__ - Compiled output (generated)
- **`AGENTS.md`** - Compiled output (generated)
## Getting Started
1. Install dependencies:
```bash
cd scripts && npm install
```
@@ -74,7 +75,7 @@ npx skills add Kadajett/agent-nestjs-skills -a claude-code -a cursor
Each rule file should follow this structure:
```markdown
````markdown
---
title: Rule Title Here
impact: MEDIUM
@@ -91,6 +92,7 @@ Brief explanation of the rule and why it matters.
```typescript
// Bad code example
```
````
**Correct (description of what's right):**
@@ -102,7 +104,6 @@ Optional explanatory text after examples.
Reference: [NestJS Documentation](https://docs.nestjs.com)
## File Naming Convention
- Files starting with `_` are special (excluded from build)
@@ -113,13 +114,13 @@ Reference: [NestJS Documentation](https://docs.nestjs.com)
## Impact Levels
| Level | Description |
|-------|-------------|
| CRITICAL | Violations cause runtime errors, security vulnerabilities, or architectural breakdown |
| HIGH | Significant impact on reliability, security, or maintainability |
| MEDIUM-HIGH | Notable impact on quality and developer experience |
| MEDIUM | Moderate impact on code quality and best practices |
| LOW-MEDIUM | Minor improvements for consistency and maintainability |
| Level | Description |
| ----------- | ------------------------------------------------------------------------------------- |
| CRITICAL | Violations cause runtime errors, security vulnerabilities, or architectural breakdown |
| HIGH | Significant impact on reliability, security, or maintainability |
| MEDIUM-HIGH | Notable impact on quality and developer experience |
| MEDIUM | Moderate impact on code quality and best practices |
| LOW-MEDIUM | Minor improvements for consistency and maintainability |
## Scripts
@@ -160,4 +161,3 @@ These NestJS skills work with:
- [Claude Code](https://claude.ai/code) - Anthropic's official CLI
- [AdaL](https://sylph.ai/adal) - Self-evolving AI coding agent with MCP support
+14 -13
View File
@@ -4,7 +4,7 @@ description: NestJS best practices and architecture patterns for building produc
license: MIT
metadata:
author: Kadajett
version: "1.1.0"
version: '1.1.0'
---
# NestJS Best Practices
@@ -24,18 +24,18 @@ Reference these guidelines when:
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Architecture | CRITICAL | `arch-` |
| 2 | Dependency Injection | CRITICAL | `di-` |
| 3 | Error Handling | HIGH | `error-` |
| 4 | Security | HIGH | `security-` |
| 5 | Performance | HIGH | `perf-` |
| 6 | Testing | MEDIUM-HIGH | `test-` |
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
| 8 | API Design | MEDIUM | `api-` |
| 9 | Microservices | MEDIUM | `micro-` |
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
| Priority | Category | Impact | Prefix |
| -------- | -------------------- | ----------- | ----------- |
| 1 | Architecture | CRITICAL | `arch-` |
| 2 | Dependency Injection | CRITICAL | `di-` |
| 3 | Error Handling | HIGH | `error-` |
| 4 | Security | HIGH | `security-` |
| 5 | Performance | HIGH | `perf-` |
| 6 | Testing | MEDIUM-HIGH | `test-` |
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
| 8 | API Design | MEDIUM | `api-` |
| 9 | Microservices | MEDIUM | `micro-` |
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
## Quick Reference
@@ -120,6 +120,7 @@ rules/_sections.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
@@ -126,7 +126,7 @@ export class UsersController {
@SerializeOptions({ type: UserResponseDto })
async findAll(): Promise<UserResponseDto[]> {
const users = await this.usersService.findAll();
return users.map(u => plainToInstance(UserResponseDto, u));
return users.map((u) => plainToInstance(UserResponseDto, u));
}
@Get(':id')
@@ -159,10 +159,7 @@ export class UsersService {
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(
@Param('id') id: string,
@Headers('X-API-Version') version: string = '1',
): Promise<any> {
async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {
return this.usersService.findOne(id, version);
}
}
@@ -1,7 +1,7 @@
---
title: Avoid Circular Dependencies
impact: CRITICAL
impactDescription: "#1 cause of runtime crashes"
impactDescription: '#1 cause of runtime crashes'
tags: architecture, modules, dependencies
---
@@ -1,7 +1,7 @@
---
title: Organize by Feature Modules
impact: CRITICAL
impactDescription: "3-5x faster onboarding and development"
impactDescription: '3-5x faster onboarding and development'
tags: architecture, modules, organization
---
@@ -1,7 +1,7 @@
---
title: Single Responsibility for Services
impact: CRITICAL
impactDescription: "40%+ improvement in testability"
impactDescription: '40%+ improvement in testability'
tags: architecture, services, single-responsibility
---
@@ -19,7 +19,7 @@ export class UserAndOrderService {
private userRepo: UserRepository,
private orderRepo: OrderRepository,
private mailer: MailService,
private payment: PaymentService,
private payment: PaymentService
) {}
async createUser(dto: CreateUserDto) {
@@ -90,7 +90,7 @@ export class OrdersController {
constructor(
private orders: OrdersService,
private payment: PaymentService,
private notifications: NotificationService,
private notifications: NotificationService
) {}
@Post()
@@ -20,7 +20,7 @@ export class OrdersService {
private emailService: EmailService,
private analyticsService: AnalyticsService,
private notificationService: NotificationService,
private loyaltyService: LoyaltyService,
private loyaltyService: LoyaltyService
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -51,7 +51,7 @@ export class OrderCreatedEvent {
public readonly orderId: string,
public readonly userId: string,
public readonly items: OrderItem[],
public readonly total: number,
public readonly total: number
) {}
}
@@ -60,17 +60,14 @@ export class OrderCreatedEvent {
export class OrdersService {
constructor(
private eventEmitter: EventEmitter2,
private repo: Repository<Order>,
private repo: Repository<Order>
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Emit event - no knowledge of consumers
this.eventEmitter.emit(
'order.created',
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
);
this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));
return order;
}
@@ -15,9 +15,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
// Complex queries in services
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic
@@ -42,9 +40,7 @@ export class UsersService {
// Custom repository with encapsulated queries
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } });
@@ -47,12 +47,7 @@ export class OrdersService {
for (const item of items) {
await manager.save(OrderItem, { orderId: order.id, ...item });
await manager.decrement(
Inventory,
{ productId: item.productId },
'stock',
item.quantity,
);
await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
}
// If this throws, everything rolls back
@@ -75,12 +70,7 @@ export class TransferService {
try {
// Debit source account
await queryRunner.manager.decrement(
Account,
{ id: fromId },
'balance',
amount,
);
await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
// Verify sufficient funds
const source = await queryRunner.manager.findOne(Account, {
@@ -91,12 +81,7 @@ export class TransferService {
}
// Credit destination account
await queryRunner.manager.increment(
Account,
{ id: toId },
'balance',
amount,
);
await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
// Log the transaction
await queryRunner.manager.save(TransactionLog, {
@@ -121,13 +106,10 @@ export class TransferService {
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
private dataSource: DataSource,
private dataSource: DataSource
) {}
async createWithProfile(
userData: CreateUserDto,
profileData: CreateProfileDto,
): Promise<User> {
async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
return this.dataSource.transaction(async (manager) => {
const user = await manager.save(User, userData);
await manager.save(Profile, { ...profileData, userId: user.id });
@@ -79,9 +79,7 @@ export class DatabaseService implements OnApplicationShutdown {
console.log(`Database service shutting down on ${signal}`);
// Close all connections gracefully
await Promise.all(
this.connections.map((conn) => conn.close()),
);
await Promise.all(this.connections.map((conn) => conn.close()));
console.log('All database connections closed');
}
@@ -150,9 +148,7 @@ export class HealthController {
throw new ServiceUnavailableException('Shutting down');
}
return this.health.check([
() => this.db.pingCheck('database'),
]);
return this.health.check([() => this.db.pingCheck('database')]);
}
}
@@ -208,10 +204,7 @@ export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
});
// Wait with timeout
await Promise.race([
this.shutdownPromise,
new Promise((resolve) => setTimeout(resolve, 30000)),
]);
await Promise.race([this.shutdownPromise, new Promise((resolve) => setTimeout(resolve, 30000))]);
}
console.log('All requests completed');
@@ -61,9 +61,7 @@ export const appConfig = registerAs('app', () => ({
// config/validation.schema.ts
export const validationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432),
@@ -137,7 +135,7 @@ export class AppService {
export class DatabaseService {
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
private dbConfig: ConfigType<typeof databaseConfig>
) {
// Full type inference!
const host = this.dbConfig.host; // string
@@ -147,12 +145,7 @@ export class DatabaseService {
// Environment files support
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env.local',
'.env',
],
envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
});
// .env.development
@@ -45,9 +45,7 @@ logger.log('User ' + userId + ' created at ' + new Date());
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger:
process.env.NODE_ENV === 'production'
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'],
process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],
});
}
@@ -82,7 +80,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(),
message,
...context,
}),
})
);
}
@@ -94,7 +92,7 @@ export class JsonLogger implements LoggerService {
message,
trace,
...context,
}),
})
);
}
@@ -105,7 +103,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(),
message,
...context,
}),
})
);
}
@@ -116,7 +114,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(),
message,
...context,
}),
})
);
}
}
@@ -166,7 +164,7 @@ export class ContextLogger {
userId: this.cls.get('userId'),
message,
...data,
}),
})
);
}
@@ -181,7 +179,7 @@ export class ContextLogger {
error: error.message,
stack: error.stack,
...data,
}),
})
);
}
}
@@ -194,10 +192,7 @@ import { LoggerModule } from 'nestjs-pino';
LoggerModule.forRoot({
pinoHttp: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
redact: ['req.headers.authorization', 'req.body.password'],
serializers: {
req: (req) => ({
@@ -55,7 +55,7 @@ export class OrdersService {
constructor(
private usersService: UsersService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
private paymentService: PaymentService
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -28,14 +28,14 @@ interface NotificationService {
@Injectable()
export class OrdersService {
constructor(
private notifications: NotificationService, // Depends on 8 methods, uses 1
private notifications: NotificationService // Depends on 8 methods, uses 1
) {}
async confirmOrder(order: Order): Promise<void> {
await this.notifications.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
`Your order ${order.id} has been confirmed.`
);
}
}
@@ -43,12 +43,12 @@ export class OrdersService {
// Testing is painful - must mock unused methods
const mockNotificationService = {
sendEmail: jest.fn(),
sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required
sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required
getDeliveryStatus: jest.fn(), // Never used, but required
retryFailed: jest.fn(), // Never used, but required
retryFailed: jest.fn(), // Never used, but required
scheduleNotification: jest.fn(), // Never used, but required
};
```
@@ -105,14 +105,14 @@ export class SendGridEmailService implements EmailSender {
@Injectable()
export class OrdersService {
constructor(
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency
@Inject(EMAIL_SENDER) private emailSender: EmailSender // Minimal dependency
) {}
async confirmOrder(order: Order): Promise<void> {
await this.emailSender.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
`Your order ${order.id} has been confirmed.`
);
}
}
@@ -150,7 +150,7 @@ type MultiChannelSender = EmailSender & SmsSender & PushSender;
export class AlertService {
constructor(
@Inject(MULTI_CHANNEL_SENDER)
private sender: EmailSender & SmsSender,
private sender: EmailSender & SmsSender
) {}
async sendCriticalAlert(user: User, message: string): Promise<void> {
@@ -178,9 +178,7 @@ export class OrdersService {
```typescript
// Shared test suite that any implementation must pass
function testPaymentGatewayContract(
createGateway: () => PaymentGateway,
) {
function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
describe('PaymentGateway contract', () => {
let gateway: PaymentGateway;
@@ -197,13 +195,11 @@ function testPaymentGatewayContract(
});
it('throws InvalidCurrencyException for unsupported currency', async () => {
await expect(gateway.charge(1000, 'INVALID'))
.rejects.toThrow(InvalidCurrencyException);
await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
});
it('throws TransactionNotFoundException for invalid refund', async () => {
await expect(gateway.refund('nonexistent'))
.rejects.toThrow(TransactionNotFoundException);
await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
});
});
}
@@ -40,7 +40,7 @@ export class UsersService {
export class UsersService {
constructor(
private readonly userRepo: UserRepository,
@Inject('CONFIG') private readonly config: ConfigType,
@Inject('CONFIG') private readonly config: ConfigType
) {}
async findAll(): Promise<User[]> {
@@ -19,7 +19,9 @@ interface PaymentGateway {
@Injectable()
export class StripeService implements PaymentGateway {
charge(amount: number) { /* ... */ }
charge(amount: number) {
/* ... */
}
}
@Injectable()
@@ -58,9 +60,7 @@ export class MockPaymentService implements PaymentGateway {
providers: [
{
provide: PAYMENT_GATEWAY,
useClass: process.env.NODE_ENV === 'test'
? MockPaymentService
: StripeService,
useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,
},
],
exports: [PAYMENT_GATEWAY],
@@ -70,9 +70,7 @@ export class PaymentModule {}
// Injection
@Injectable()
export class OrdersService {
constructor(
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
) {}
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
async createOrder(dto: CreateOrderDto) {
await this.payment.charge(dto.amount);
@@ -88,7 +88,7 @@ export class UsersController {
export class EntityNotFoundException extends Error {
constructor(
public readonly entity: string,
public readonly id: string,
public readonly id: string
) {
super(`${entity} with ID "${id}" not found`);
}
@@ -95,20 +95,11 @@ export class AllExceptionsFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
const message = exception instanceof HttpException ? exception.message : 'Internal server error';
this.logger.error(
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : exception,
);
this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);
response.status(status).json({
statusCode: status,
@@ -120,10 +111,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
}
// Register globally in main.ts
app.useGlobalFilters(
new AllExceptionsFilter(app.get(Logger)),
new DomainExceptionFilter(),
);
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());
// Or via module
@Module({
@@ -64,11 +64,7 @@ import { BullModule } from '@nestjs/bullmq';
},
},
}),
BullModule.registerQueue(
{ name: 'email' },
{ name: 'reports' },
{ name: 'notifications' },
),
BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),
],
})
export class QueueModule {}
@@ -76,9 +72,7 @@ export class QueueModule {}
// Producer: Add jobs to queue
@Injectable()
export class ReportsService {
constructor(
@InjectQueue('reports') private reportsQueue: Queue,
) {}
constructor(@InjectQueue('reports') private reportsQueue: Queue) {}
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
// Return immediately, process in background
@@ -176,7 +170,7 @@ export class NotificationService {
{
attempts: 5,
backoff: { type: 'exponential', delay: 5000 },
},
}
);
}
}
@@ -194,7 +188,7 @@ export class ScheduledJobsService implements OnModuleInit {
{
repeat: { cron: '0 0 * * *' },
jobId: 'daily-cleanup', // Prevent duplicates
},
}
);
// Send digest every hour
@@ -204,7 +198,7 @@ export class ScheduledJobsService implements OnModuleInit {
{
repeat: { every: 60 * 60 * 1000 },
jobId: 'hourly-digest',
},
}
);
}
}
@@ -64,7 +64,7 @@ export class DatabaseService implements OnModuleInit {
export class CacheWarmerService implements OnApplicationBootstrap {
constructor(
private cache: CacheService,
private products: ProductsService,
private products: ProductsService
) {}
async onApplicationBootstrap(): Promise<void> {
@@ -81,10 +81,7 @@ export class ModuleLoaderService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async load<T>(
key: string,
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
): Promise<ModuleRef> {
async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {
if (!this.loadedModules.has(key)) {
const module = await importFn();
const moduleType = 'default' in module ? module.default : module;
@@ -51,9 +51,7 @@ export class UsersService {
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
stores: [
new KeyvRedis(config.get('REDIS_URL')),
],
stores: [new KeyvRedis(config.get('REDIS_URL'))],
ttl: 60 * 1000, // Default 60s
}),
}),
@@ -66,7 +64,7 @@ export class AppModule {}
export class ProductsService {
constructor(
@Inject(CACHE_MANAGER) private cache: Cache,
private productsRepo: ProductRepository,
private productsRepo: ProductRepository
) {}
async getPopular(): Promise<Product[]> {
@@ -117,10 +115,7 @@ export class CacheInvalidationService {
@OnEvent('product.updated')
@OnEvent('product.deleted')
async invalidateProductCaches(event: ProductEvent) {
await Promise.all([
this.cache.del('products:popular'),
this.cache.del(`product:${event.productId}`),
]);
await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);
}
}
```
@@ -111,7 +111,7 @@ export class AuthService {
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private config: ConfigService,
private usersService: UsersService,
private usersService: UsersService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
@@ -47,15 +47,12 @@ export class AdminController {
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
private reflector: Reflector
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check for @Public() decorator
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
@@ -85,10 +82,7 @@ export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);
if (!requiredRoles) return true;
@@ -30,9 +30,9 @@ export class UsersController {
// DTOs without validation decorators
export class CreateUserDto {
name: string; // No validation
email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999
name: string; // No validation
email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999
}
```
@@ -45,13 +45,13 @@ async function bootstrap() {
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types
transformOptions: {
enableImplicitConversion: true,
},
}),
})
);
await app.listen(3000);
@@ -61,7 +61,7 @@ describe('UsersController (e2e)', () => {
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
})
);
await app.init();
@@ -97,9 +97,7 @@ describe('UsersController (e2e)', () => {
describe('/users/:id (GET)', () => {
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/non-existent-id')
.expect(404);
return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);
});
});
});
@@ -127,9 +125,7 @@ describe('Protected Routes (e2e)', () => {
});
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/users/me')
.expect(401);
return request(app.getHttpServer()).get('/users/me').expect(401);
});
it('should return user profile with valid token', () => {
@@ -84,9 +84,7 @@ describe('WeatherService', () => {
});
it('should handle API timeout', async () => {
httpService.get.mockReturnValue(
throwError(() => new Error('ETIMEDOUT')),
);
httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
});
@@ -95,7 +93,7 @@ describe('WeatherService', () => {
httpService.get.mockReturnValue(
throwError(() => ({
response: { status: 429, data: { message: 'Rate limited' } },
})),
}))
);
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
@@ -117,10 +115,7 @@ describe('UsersService', () => {
};
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],
}).compile();
service = module.get(UsersService);
@@ -86,9 +86,7 @@ describe('UsersService', () => {
it('should throw on duplicate email', async () => {
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
await expect(
service.create({ name: 'Test', email: 'test@test.com' }),
).rejects.toThrow(ConflictException);
await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);
});
});
@@ -98,7 +98,7 @@ function parseFrontmatter(content: string): { frontmatter: RuleFrontmatter | nul
return {
frontmatter: frontmatter as RuleFrontmatter,
body: body.trim()
body: body.trim(),
};
}
@@ -118,8 +118,7 @@ function readMetadata(): any {
function readRules(): Rule[] {
const rulesDir = path.join(__dirname, '..', 'rules');
const files = fs.readdirSync(rulesDir)
.filter(f => f.endsWith('.md') && !f.startsWith('_'));
const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
const rules: Rule[] = [];
@@ -144,7 +143,7 @@ function readRules(): Rule[] {
frontmatter,
content: body,
category: category.name,
categorySection: category.section
categorySection: category.section,
});
}
+19 -1
View File
@@ -11,6 +11,7 @@ Apply these rules when writing or reviewing Next.js code.
## File Conventions
See [file-conventions.md](./file-conventions.md) for:
- Project structure and special files
- Route segments (dynamic, catch-all, groups)
- Parallel and intercepting routes
@@ -21,6 +22,7 @@ See [file-conventions.md](./file-conventions.md) for:
Detect invalid React Server Component patterns.
See [rsc-boundaries.md](./rsc-boundaries.md) for:
- Async client component detection (invalid)
- Non-serializable props detection
- Server Action exceptions
@@ -30,6 +32,7 @@ See [rsc-boundaries.md](./rsc-boundaries.md) for:
Next.js 15+ async API changes.
See [async-patterns.md](./async-patterns.md) for:
- Async `params` and `searchParams`
- Async `cookies()` and `headers()`
- Migration codemod
@@ -37,18 +40,21 @@ See [async-patterns.md](./async-patterns.md) for:
## Runtime Selection
See [runtime-selection.md](./runtime-selection.md) for:
- Default to Node.js runtime
- When Edge runtime is appropriate
## Directives
See [directives.md](./directives.md) for:
- `'use client'`, `'use server'` (React)
- `'use cache'` (Next.js)
## Functions
See [functions.md](./functions.md) for:
- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`
- Server functions: `cookies`, `headers`, `draftMode`, `after`
- Generate functions: `generateStaticParams`, `generateMetadata`
@@ -56,6 +62,7 @@ See [functions.md](./functions.md) for:
## Error Handling
See [error-handling.md](./error-handling.md) for:
- `error.tsx`, `global-error.tsx`, `not-found.tsx`
- `redirect`, `permanentRedirect`, `notFound`
- `forbidden`, `unauthorized` (auth errors)
@@ -64,6 +71,7 @@ See [error-handling.md](./error-handling.md) for:
## Data Patterns
See [data-patterns.md](./data-patterns.md) for:
- Server Components vs Server Actions vs Route Handlers
- Avoiding data waterfalls (`Promise.all`, Suspense, preload)
- Client component data fetching
@@ -71,6 +79,7 @@ See [data-patterns.md](./data-patterns.md) for:
## Route Handlers
See [route-handlers.md](./route-handlers.md) for:
- `route.ts` basics
- GET handler conflicts with `page.tsx`
- Environment behavior (no React DOM)
@@ -79,6 +88,7 @@ See [route-handlers.md](./route-handlers.md) for:
## Metadata & OG Images
See [metadata.md](./metadata.md) for:
- Static and dynamic metadata
- `generateMetadata` function
- OG image generation with `next/og`
@@ -87,6 +97,7 @@ See [metadata.md](./metadata.md) for:
## Image Optimization
See [image.md](./image.md) for:
- Always use `next/image` over `<img>`
- Remote images configuration
- Responsive `sizes` attribute
@@ -96,6 +107,7 @@ See [image.md](./image.md) for:
## Font Optimization
See [font.md](./font.md) for:
- `next/font` setup
- Google Fonts, local fonts
- Tailwind CSS integration
@@ -104,6 +116,7 @@ See [font.md](./font.md) for:
## Bundling
See [bundling.md](./bundling.md) for:
- Server-incompatible packages
- CSS imports (not link tags)
- Polyfills (already included)
@@ -113,6 +126,7 @@ See [bundling.md](./bundling.md) for:
## Scripts
See [scripts.md](./scripts.md) for:
- `next/script` vs native script tags
- Inline scripts need `id`
- Loading strategies
@@ -121,6 +135,7 @@ See [scripts.md](./scripts.md) for:
## Hydration Errors
See [hydration-error.md](./hydration-error.md) for:
- Common causes (browser APIs, dates, invalid HTML)
- Debugging with error overlay
- Fixes for each cause
@@ -128,12 +143,14 @@ See [hydration-error.md](./hydration-error.md) for:
## Suspense Boundaries
See [suspense-boundaries.md](./suspense-boundaries.md) for:
- CSR bailout with `useSearchParams` and `usePathname`
- Which hooks require Suspense boundaries
## Parallel & Intercepting Routes
See [parallel-routes.md](./parallel-routes.md) for:
- Modal patterns with `@slot` and `(.)` interceptors
- `default.tsx` for fallbacks
- Closing modals correctly with `router.back()`
@@ -141,6 +158,7 @@ See [parallel-routes.md](./parallel-routes.md) for:
## Self-Hosting
See [self-hosting.md](./self-hosting.md) for:
- `output: 'standalone'` for Docker
- Cache handlers for multi-instance ISR
- What works vs needs extra setup
@@ -148,6 +166,6 @@ See [self-hosting.md](./self-hosting.md) for:
## Debug Tricks
See [debug-tricks.md](./debug-tricks.md) for:
- MCP endpoint for AI-assisted debugging
- Rebuild specific routes with `--debug-build-paths`
@@ -9,21 +9,18 @@ Always type them as `Promise<...>` and await them.
### Pages and Layouts
```tsx
type Props = { params: Promise<{ slug: string }> }
type Props = { params: Promise<{ slug: string }> };
export default async function Page({ params }: Props) {
const { slug } = await params
const { slug } = await params;
}
```
### Route Handlers
```tsx
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
}
```
@@ -31,13 +28,13 @@ export async function GET(
```tsx
type Props = {
params: Promise<{ slug: string }>
searchParams: Promise<{ query?: string }>
}
params: Promise<{ slug: string }>;
searchParams: Promise<{ query?: string }>;
};
export default async function Page({ params, searchParams }: Props) {
const { slug } = await params
const { query } = await searchParams
const { slug } = await params;
const { query } = await searchParams;
}
```
@@ -46,37 +43,37 @@ export default async function Page({ params, searchParams }: Props) {
Use `React.use()` for non-async components:
```tsx
import { use } from 'react'
import { use } from 'react';
type Props = { params: Promise<{ slug: string }> }
type Props = { params: Promise<{ slug: string }> };
export default function Page({ params }: Props) {
const { slug } = use(params)
const { slug } = use(params);
}
```
### generateMetadata
```tsx
type Props = { params: Promise<{ slug: string }> }
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
return { title: slug }
const { slug } = await params;
return { title: slug };
}
```
## Async Cookies and Headers
```tsx
import { cookies, headers } from 'next/headers'
import { cookies, headers } from 'next/headers';
export default async function Page() {
const cookieStore = await cookies()
const headersList = await headers()
const cookieStore = await cookies();
const headersList = await headers();
const theme = cookieStore.get('theme')
const userAgent = headersList.get('user-agent')
const theme = cookieStore.get('theme');
const userAgent = headersList.get('user-agent');
}
```
+28 -26
View File
@@ -21,21 +21,21 @@ If the package is only needed on client:
```tsx
// Bad: Fails - package uses window
import SomeChart from 'some-chart-library'
import SomeChart from 'some-chart-library';
export default function Page() {
return <SomeChart />
return <SomeChart />;
}
// Good: Use dynamic import with ssr: false
import dynamic from 'next/dynamic'
import dynamic from 'next/dynamic';
const SomeChart = dynamic(() => import('some-chart-library'), {
ssr: false,
})
});
export default function Page() {
return <SomeChart />
return <SomeChart />;
}
```
@@ -47,10 +47,11 @@ For packages that should run on server but have bundling issues:
// next.config.js
module.exports = {
serverExternalPackages: ['problematic-package'],
}
};
```
Use this for:
- Packages with native bindings (sharp, bcrypt)
- Packages that don't bundle well (some ORMs)
- Packages with circular dependencies
@@ -61,19 +62,19 @@ Wrap the entire usage in a client component:
```tsx
// components/ChartWrapper.tsx
'use client'
'use client';
import { Chart } from 'chart-library'
import { Chart } from 'chart-library';
export function ChartWrapper(props) {
return <Chart {...props} />
return <Chart {...props} />;
}
// app/page.tsx (server component)
import { ChartWrapper } from '@/components/ChartWrapper'
import { ChartWrapper } from '@/components/ChartWrapper';
export default function Page() {
return <ChartWrapper data={data} />
return <ChartWrapper data={data} />;
}
```
@@ -83,13 +84,13 @@ Import CSS files instead of using `<link>` tags. Next.js handles bundling and op
```tsx
// Bad: Manual link tag
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="/styles.css" />;
// Good: Import CSS
import './styles.css'
import './styles.css';
// Good: CSS Modules
import styles from './Button.module.css'
import styles from './Button.module.css';
```
## Polyfills
@@ -121,21 +122,21 @@ Module not found: ESM packages need to be imported
// next.config.js
module.exports = {
transpilePackages: ['some-esm-package', 'another-package'],
}
};
```
## Common Problematic Packages
| Package | Issue | Solution |
|---------|-------|----------|
| `sharp` | Native bindings | `serverExternalPackages: ['sharp']` |
| `bcrypt` | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs` |
| `canvas` | Native bindings | `serverExternalPackages: ['canvas']` |
| `recharts` | Uses window | `dynamic(() => import('recharts'), { ssr: false })` |
| `react-quill` | Uses document | `dynamic(() => import('react-quill'), { ssr: false })` |
| `mapbox-gl` | Uses window | `dynamic(() => import('mapbox-gl'), { ssr: false })` |
| `monaco-editor` | Uses window | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |
| `lottie-web` | Uses document | `dynamic(() => import('lottie-react'), { ssr: false })` |
| Package | Issue | Solution |
| --------------- | --------------- | --------------------------------------------------------------- |
| `sharp` | Native bindings | `serverExternalPackages: ['sharp']` |
| `bcrypt` | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs` |
| `canvas` | Native bindings | `serverExternalPackages: ['canvas']` |
| `recharts` | Uses window | `dynamic(() => import('recharts'), { ssr: false })` |
| `react-quill` | Uses document | `dynamic(() => import('react-quill'), { ssr: false })` |
| `mapbox-gl` | Uses window | `dynamic(() => import('mapbox-gl'), { ssr: false })` |
| `monaco-editor` | Uses window | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |
| `lottie-web` | Uses document | `dynamic(() => import('lottie-react'), { ssr: false })` |
## Bundle Analysis
@@ -146,6 +147,7 @@ next experimental-analyze
```
This opens an interactive UI to:
- Filter by route, environment (client/server), and type
- Inspect module sizes and import chains
- View treemap visualization
@@ -174,7 +176,7 @@ module.exports = {
webpack: (config) => {
// custom webpack config
},
}
};
```
Reference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack
@@ -33,17 +33,20 @@ async function UsersPage() {
const users = await db.user.findMany();
// Or fetch from external API
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
```
**Benefits**:
- No API to maintain
- No client-server waterfall
- Secrets stay on server
@@ -89,12 +92,14 @@ export default function NewPost() {
```
**Benefits**:
- End-to-end type safety
- Progressive enhancement (works without JS)
- Automatic request handling
- Integrated with React transitions
**Constraints**:
- POST only (no GET caching semantics)
- Internal use only (no external access)
- Cannot return non-serializable data
@@ -122,12 +127,14 @@ export async function POST(request: NextRequest) {
```
**When to use**:
- External API access (mobile apps, third parties)
- Webhooks from external services
- GET endpoints that need HTTP caching
- OpenAPI/Swagger documentation needed
**When NOT to use**:
- Internal data fetching (use Server Components)
- Mutations from your UI (use Server Actions)
@@ -138,8 +145,8 @@ export async function POST(request: NextRequest) {
```tsx
// Bad: Sequential waterfalls
async function Dashboard() {
const user = await getUser(); // Wait...
const posts = await getPosts(); // Then wait...
const user = await getUser(); // Wait...
const posts = await getPosts(); // Then wait...
const comments = await getComments(); // Then wait...
return <div>...</div>;
@@ -151,11 +158,7 @@ async function Dashboard() {
```tsx
// Good: Parallel fetching
async function Dashboard() {
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
]);
const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]);
return <div>...</div>;
}
@@ -238,7 +241,7 @@ async function Page() {
}
// Client Component
'use client';
('use client');
function ClientComponent({ initialData }) {
const [data, setData] = useState(initialData);
// ...
@@ -256,7 +259,7 @@ function ClientComponent() {
useEffect(() => {
fetch('/api/data')
.then(r => r.json())
.then((r) => r.json())
.then(setData);
}, []);
@@ -289,9 +292,9 @@ function ClientComponent() {
## Quick Reference
| Pattern | Use Case | HTTP Method | Caching |
|---------|----------|-------------|---------|
| Server Component fetch | Internal reads | Any | Full Next.js caching |
| Server Action | Mutations, form submissions | POST only | No |
| Route Handler | External APIs, webhooks | Any | GET can be cached |
| Client fetch to API | Client-side reads | Any | HTTP cache headers |
| Pattern | Use Case | HTTP Method | Caching |
| ---------------------- | --------------------------- | ----------- | -------------------- |
| Server Component fetch | Internal reads | Any | Full Next.js caching |
| Server Action | Mutations, form submissions | POST only | No |
| Route Handler | External APIs, webhooks | Any | GET can be cached |
| Client fetch to API | Client-side reads | Any | HTTP cache headers |
@@ -35,42 +35,58 @@ curl -X POST http://localhost:<port>/_next/mcp \
### Available Tools
#### `get_errors`
Get current errors from dev server (build errors, runtime errors with source-mapped stacks):
```json
{ "name": "get_errors", "arguments": {} }
```
#### `get_routes`
Discover all routes by scanning filesystem:
```json
{ "name": "get_routes", "arguments": {} }
// Optional: { "name": "get_routes", "arguments": { "routerType": "app" } }
```
Returns: `{ "appRouter": ["/", "/api/users/[id]", ...], "pagesRouter": [...] }`
#### `get_project_metadata`
Get project path and dev server URL:
```json
{ "name": "get_project_metadata", "arguments": {} }
```
Returns: `{ "projectPath": "/path/to/project", "devServerUrl": "http://localhost:3000" }`
#### `get_page_metadata`
Get runtime metadata about current page render (requires active browser session):
```json
{ "name": "get_page_metadata", "arguments": {} }
```
Returns segment trie data showing layouts, boundaries, and page components.
#### `get_logs`
Get path to Next.js development log file:
```json
{ "name": "get_logs", "arguments": {} }
```
Returns path to `<distDir>/logs/next-development.log`
#### `get_server_action_by_id`
Locate a Server Action by ID:
```json
{ "name": "get_server_action_by_id", "arguments": { "actionId": "<action-id>" } }
```
@@ -100,6 +116,7 @@ next build --debug-build-paths "/blog/[slug]"
```
Use this to:
- Quickly verify a build fix without full rebuild
- Debug static generation issues for specific pages
- Iterate faster on build errors
@@ -7,18 +7,19 @@ These are React directives, not Next.js specific.
### `'use client'`
Marks a component as a Client Component. Required for:
- React hooks (`useState`, `useEffect`, etc.)
- Event handlers (`onClick`, `onChange`)
- Browser APIs (`window`, `localStorage`)
```tsx
'use client'
'use client';
import { useState } from 'react'
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
```
@@ -29,7 +30,7 @@ Reference: https://react.dev/reference/rsc/use-client
Marks a function as a Server Action. Can be passed to Client Components.
```tsx
'use server'
'use server';
export async function submitForm(formData: FormData) {
// Runs on server
@@ -41,10 +42,10 @@ Or inline within a Server Component:
```tsx
export default function Page() {
async function submit() {
'use server'
'use server';
// Runs on server
}
return <form action={submit}>...</form>
return <form action={submit}>...</form>;
}
```
@@ -59,10 +60,10 @@ Reference: https://react.dev/reference/rsc/use-server
Marks a function or component for caching. Part of Next.js Cache Components.
```tsx
'use cache'
'use cache';
export async function getCachedData() {
return await fetchData()
return await fetchData();
}
```
@@ -11,21 +11,15 @@ Reference: https://nextjs.org/docs/app/getting-started/error-handling
Catches errors in a route segment and its children:
```tsx
'use client'
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
);
}
```
@@ -36,15 +30,9 @@ export default function Error({
Catches errors in root layout:
```tsx
'use client'
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
return (
<html>
<body>
@@ -52,7 +40,7 @@ export default function GlobalError({
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
);
}
```
@@ -107,6 +95,7 @@ async function createPost(formData: FormData) {
```
Same applies to:
- `redirect()` - 307 temporary redirect
- `permanentRedirect()` - 308 permanent redirect
- `notFound()` - 404 not found
@@ -116,15 +105,15 @@ Same applies to:
Use `unstable_rethrow()` to re-throw these errors in catch blocks:
```tsx
import { unstable_rethrow } from 'next/navigation'
import { unstable_rethrow } from 'next/navigation';
async function action() {
try {
// ...
redirect('/success')
redirect('/success');
} catch (error) {
unstable_rethrow(error) // Re-throws Next.js internal errors
return { error: 'Something went wrong' }
unstable_rethrow(error); // Re-throws Next.js internal errors
return { error: 'Something went wrong' };
}
}
```
@@ -132,13 +121,13 @@ async function action() {
## Redirects
```tsx
import { redirect, permanentRedirect } from 'next/navigation'
import { redirect, permanentRedirect } from 'next/navigation';
// 307 Temporary - use for most cases
redirect('/new-path')
redirect('/new-path');
// 308 Permanent - use for URL migrations (cached by browsers)
permanentRedirect('/new-url')
permanentRedirect('/new-url');
```
## Auth Errors
@@ -146,20 +135,20 @@ permanentRedirect('/new-url')
Trigger auth-related error pages:
```tsx
import { forbidden, unauthorized } from 'next/navigation'
import { forbidden, unauthorized } from 'next/navigation';
async function Page() {
const session = await getSession()
const session = await getSession();
if (!session) {
unauthorized() // Renders unauthorized.tsx (401)
unauthorized(); // Renders unauthorized.tsx (401)
}
if (!session.hasAccess) {
forbidden() // Renders forbidden.tsx (403)
forbidden(); // Renders forbidden.tsx (403)
}
return <Dashboard />
return <Dashboard />;
}
```
@@ -168,12 +157,12 @@ Create corresponding error pages:
```tsx
// app/forbidden.tsx
export default function Forbidden() {
return <div>You don't have access to this resource</div>
return <div>You don't have access to this resource</div>;
}
// app/unauthorized.tsx
export default function Unauthorized() {
return <div>Please log in to continue</div>
return <div>Please log in to continue</div>;
}
```
@@ -190,24 +179,24 @@ export default function NotFound() {
<h2>Not Found</h2>
<p>Could not find the requested resource</p>
</div>
)
);
}
```
### Triggering Not Found
```tsx
import { notFound } from 'next/navigation'
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await getPost(id)
const { id } = await params;
const post = await getPost(id);
if (!post) {
notFound() // Renders closest not-found.tsx
notFound(); // Renders closest not-found.tsx
}
return <div>{post.title}</div>
return <div>{post.title}</div>;
}
```
@@ -27,16 +27,16 @@ app/
## Special Files
| File | Purpose |
|------|---------|
| `page.tsx` | UI for a route segment |
| `layout.tsx` | Shared UI for segment and children |
| `loading.tsx` | Loading UI (Suspense boundary) |
| `error.tsx` | Error UI (Error boundary) |
| `not-found.tsx` | 404 UI |
| `route.ts` | API endpoint |
| `template.tsx` | Like layout but re-renders on navigation |
| `default.tsx` | Fallback for parallel routes |
| File | Purpose |
| --------------- | ---------------------------------------- |
| `page.tsx` | UI for a route segment |
| `layout.tsx` | Shared UI for segment and children |
| `loading.tsx` | Loading UI (Suspense boundary) |
| `error.tsx` | Error UI (Error boundary) |
| `not-found.tsx` | 404 UI |
| `route.ts` | API endpoint |
| `template.tsx` | Like layout but re-renders on navigation |
| `default.tsx` | Fallback for parallel routes |
## Route Segments
@@ -74,6 +74,7 @@ app/
```
Conventions:
- `(.)` - same level
- `(..)` - one level up
- `(..)(..)` - two levels up
@@ -128,10 +129,10 @@ export const proxyConfig = {
};
```
| Version | File | Export | Config |
|---------|------|--------|--------|
| v14-15 | `middleware.ts` | `middleware()` | `config` |
| v16+ | `proxy.ts` | `proxy()` | `proxyConfig` |
| Version | File | Export | Config |
| ------- | --------------- | -------------- | ------------- |
| v14-15 | `middleware.ts` | `middleware()` | `config` |
| v16+ | `proxy.ts` | `proxy()` | `proxyConfig` |
**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.
+28 -27
View File
@@ -6,44 +6,45 @@ Use `next/font` for automatic font optimization with zero layout shift.
```tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] })
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
);
}
```
## Multiple Fonts
```tsx
import { Inter, Roboto_Mono } from 'next/font/google'
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
})
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
);
}
```
Use in CSS:
```css
body {
font-family: var(--font-inter);
@@ -61,35 +62,35 @@ code {
const inter = Inter({
subsets: ['latin'],
weight: '400',
})
});
// Multiple weights
const inter = Inter({
subsets: ['latin'],
weight: ['400', '500', '700'],
})
});
// Variable font (recommended) - includes all weights
const inter = Inter({
subsets: ['latin'],
// No weight needed - variable fonts support all weights
})
});
// With italic
const inter = Inter({
subsets: ['latin'],
style: ['normal', 'italic'],
})
});
```
## Local Fonts
```tsx
import localFont from 'next/font/local'
import localFont from 'next/font/local';
const myFont = localFont({
src: './fonts/MyFont.woff2',
})
});
// Multiple files for different weights
const myFont = localFont({
@@ -105,32 +106,32 @@ const myFont = localFont({
style: 'normal',
},
],
})
});
// Variable font
const myFont = localFont({
src: './fonts/MyFont-Variable.woff2',
variable: '--font-my-font',
})
});
```
## Tailwind CSS Integration
```tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
)
);
}
```
@@ -144,7 +145,7 @@ module.exports = {
},
},
},
}
};
```
## Preloading Subsets
@@ -153,10 +154,10 @@ Only load needed character subsets:
```tsx
// Latin only (most common)
const inter = Inter({ subsets: ['latin'] })
const inter = Inter({ subsets: ['latin'] });
// Multiple subsets
const inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] })
const inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] });
```
## Display Strategy
@@ -167,7 +168,7 @@ Control font loading behavior:
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Default - shows fallback, swaps when loaded
})
});
// Options:
// 'auto' - browser decides
@@ -231,15 +232,15 @@ const inter = Inter({ subsets: ['latin'] })
```tsx
// For component-specific fonts, export from a shared file
// lib/fonts.ts
import { Inter, Playfair_Display } from 'next/font/google'
import { Inter, Playfair_Display } from 'next/font/google';
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
export const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' })
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
export const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' });
// components/Heading.tsx
import { playfair } from '@/lib/fonts'
import { playfair } from '@/lib/fonts';
export function Heading({ children }) {
return <h1 className={playfair.className}>{children}</h1>
return <h1 className={playfair.className}>{children}</h1>;
}
```
+44 -44
View File
@@ -6,45 +6,45 @@ Reference: https://nextjs.org/docs/app/api-reference/functions
## Navigation Hooks (Client)
| Hook | Purpose | Reference |
|------|---------|-----------|
| `useRouter` | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router) |
| `usePathname` | Get current pathname | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname) |
| `useSearchParams` | Read URL search parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params) |
| `useParams` | Access dynamic route parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params) |
| `useSelectedLayoutSegment` | Active child segment (one level) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) |
| `useSelectedLayoutSegments` | All active segments below layout | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |
| `useLinkStatus` | Check link prefetch status | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status) |
| `useReportWebVitals` | Report Core Web Vitals metrics | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals) |
| Hook | Purpose | Reference |
| --------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `useRouter` | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router) |
| `usePathname` | Get current pathname | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname) |
| `useSearchParams` | Read URL search parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params) |
| `useParams` | Access dynamic route parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params) |
| `useSelectedLayoutSegment` | Active child segment (one level) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) |
| `useSelectedLayoutSegments` | All active segments below layout | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |
| `useLinkStatus` | Check link prefetch status | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status) |
| `useReportWebVitals` | Report Core Web Vitals metrics | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals) |
## Server Functions
| Function | Purpose | Reference |
|----------|---------|-----------|
| `cookies` | Read/write cookies | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies) |
| `headers` | Read request headers | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers) |
| `draftMode` | Enable preview of unpublished CMS content | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |
| `after` | Run code after response finishes streaming | [Docs](https://nextjs.org/docs/app/api-reference/functions/after) |
| Function | Purpose | Reference |
| ------------ | -------------------------------------------- | ---------------------------------------------------------------------- |
| `cookies` | Read/write cookies | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies) |
| `headers` | Read request headers | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers) |
| `draftMode` | Enable preview of unpublished CMS content | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |
| `after` | Run code after response finishes streaming | [Docs](https://nextjs.org/docs/app/api-reference/functions/after) |
| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |
| `userAgent` | Parse User-Agent header | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent) |
| `userAgent` | Parse User-Agent header | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent) |
## Generate Functions
| Function | Purpose | Reference |
|----------|---------|-----------|
| `generateStaticParams` | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) |
| `generateMetadata` | Dynamic metadata | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) |
| `generateViewport` | Dynamic viewport config | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport) |
| `generateSitemaps` | Multiple sitemaps for large sites | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) |
| `generateImageMetadata` | Multiple OG images per route | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |
| Function | Purpose | Reference |
| ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |
| `generateStaticParams` | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) |
| `generateMetadata` | Dynamic metadata | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) |
| `generateViewport` | Dynamic viewport config | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport) |
| `generateSitemaps` | Multiple sitemaps for large sites | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) |
| `generateImageMetadata` | Multiple OG images per route | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |
## Request/Response
| Function | Purpose | Reference |
|----------|---------|-----------|
| `NextRequest` | Extended Request with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request) |
| `NextResponse` | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response) |
| `ImageResponse` | Generate OG images | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |
| Function | Purpose | Reference |
| --------------- | ------------------------------ | -------------------------------------------------------------------------- |
| `NextRequest` | Extended Request with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request) |
| `NextResponse` | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response) |
| `ImageResponse` | Generate OG images | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |
## Common Examples
@@ -54,30 +54,30 @@ Use `next/link` for internal navigation instead of `<a>` tags.
```tsx
// Bad: Plain anchor tag
<a href="/about">About</a>
<a href="/about">About</a>;
// Good: Next.js Link
import Link from 'next/link'
import Link from 'next/link';
<Link href="/about">About</Link>
<Link href="/about">About</Link>;
```
Active link styling:
```tsx
'use client'
'use client';
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export function NavLink({ href, children }) {
const pathname = usePathname()
const pathname = usePathname();
return (
<Link href={href} className={pathname === href ? 'active' : ''}>
{children}
</Link>
)
);
}
```
@@ -86,23 +86,23 @@ export function NavLink({ href, children }) {
```tsx
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
```
### After Response
```tsx
import { after } from 'next/server'
import { after } from 'next/server';
export async function POST(request: Request) {
const data = await processRequest(request)
const data = await processRequest(request);
after(async () => {
await logAnalytics(data)
})
await logAnalytics(data);
});
return Response.json({ success: true })
return Response.json({ success: true });
}
```
@@ -17,16 +17,16 @@ In development, click the hydration error to see the server/client diff.
```tsx
// Bad: Causes mismatch - window doesn't exist on server
<div>{window.innerWidth}</div>
<div>{window.innerWidth}</div>;
// Good: Use client component with mounted check
'use client'
import { useState, useEffect } from 'react'
('use client');
import { useState, useEffect } from 'react';
export function ClientOnly({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
return mounted ? children : null
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return mounted ? children : null;
}
```
@@ -36,12 +36,12 @@ Server and client may be in different timezones:
```tsx
// Bad: Causes mismatch
<span>{new Date().toLocaleString()}</span>
<span>{new Date().toLocaleString()}</span>;
// Good: Render on client only
'use client'
const [time, setTime] = useState<string>()
useEffect(() => setTime(new Date().toLocaleString()), [])
('use client');
const [time, setTime] = useState<string>();
useEffect(() => setTime(new Date().toLocaleString()), []);
```
### Random Values or IDs
@@ -78,14 +78,9 @@ Scripts that modify DOM during hydration.
```tsx
// Good: Use next/script with afterInteractive
import Script from 'next/script'
import Script from 'next/script';
export default function Page() {
return (
<Script
src="https://example.com/script.js"
strategy="afterInteractive"
/>
)
return <Script src="https://example.com/script.js" strategy="afterInteractive" />;
}
```
+9 -9
View File
@@ -6,11 +6,11 @@ Use `next/image` for automatic image optimization.
```tsx
// Bad: Avoid native img
<img src="/hero.png" alt="Hero" />
<img src="/hero.png" alt="Hero" />;
// Good: Use next/image
import Image from 'next/image'
<Image src="/hero.png" alt="Hero" width={800} height={400} />
import Image from 'next/image';
<Image src="/hero.png" alt="Hero" width={800} height={400} />;
```
## Required Props
@@ -51,7 +51,7 @@ module.exports = {
},
],
},
}
};
```
## Responsive Images
@@ -155,19 +155,19 @@ When using `output: 'export'`, use `unoptimized` or custom loader:
```tsx
// Option 1: Disable optimization
<Image src="/hero.png" alt="Hero" width={800} height={400} unoptimized />
<Image src="/hero.png" alt="Hero" width={800} height={400} unoptimized />;
// Option 2: Global config
// next.config.js
module.exports = {
output: 'export',
images: { unoptimized: true },
}
};
// Option 3: Custom loader (Cloudinary, Imgix, etc.)
const cloudinaryLoader = ({ src, width, quality }) => {
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`
}
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;
};
<Image loader={cloudinaryLoader} src="sample.jpg" alt="Sample" width={800} height={400} />
<Image loader={cloudinaryLoader} src="sample.jpg" alt="Sample" width={800} height={400} />;
```
+64 -72
View File
@@ -7,6 +7,7 @@ Add SEO metadata to Next.js pages using the Metadata API.
The `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.
If the target page has `'use client'`:
1. Remove `'use client'` if possible, move client logic to child components
2. Or extract metadata to a parent Server Component layout
3. Or split the file: Server Component with metadata imports Client Components
@@ -14,25 +15,25 @@ If the target page has `'use client'`:
## Static Metadata
```tsx
import type { Metadata } from 'next'
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Page Title',
description: 'Page description for search engines',
}
};
```
## Dynamic Metadata
```tsx
import type { Metadata } from 'next'
import type { Metadata } from 'next';
type Props = { params: Promise<{ slug: string }> }
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return { title: post.title, description: post.description }
const { slug } = await params;
const post = await getPost(slug);
return { title: post.title, description: post.description };
}
```
@@ -41,11 +42,11 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
Use React `cache()` when the same data is needed for both metadata and page:
```tsx
import { cache } from 'react'
import { cache } from 'react';
export const getPost = cache(async (slug: string) => {
return await db.posts.findFirst({ where: { slug } })
})
return await db.posts.findFirst({ where: { slug } });
});
```
## Viewport
@@ -53,17 +54,17 @@ export const getPost = cache(async (slug: string) => {
Separate from metadata for streaming support:
```tsx
import type { Viewport } from 'next'
import type { Viewport } from 'next';
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#000000',
}
};
// Or dynamic
export function generateViewport({ params }): Viewport {
return { themeColor: getThemeColor(params) }
return { themeColor: getThemeColor(params) };
}
```
@@ -74,7 +75,7 @@ In root layout for consistent naming:
```tsx
export const metadata: Metadata = {
title: { default: 'Site Name', template: '%s | Site Name' },
}
};
```
## Metadata File Conventions
@@ -83,16 +84,16 @@ Reference: https://nextjs.org/docs/app/getting-started/project-structure#metadat
Place these files in `app/` directory (or route segments):
| File | Purpose |
|------|---------|
| `favicon.ico` | Favicon |
| `icon.png` / `icon.svg` | App icon |
| `apple-icon.png` | Apple app icon |
| `opengraph-image.png` | OG image |
| `twitter-image.png` | Twitter card image |
| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
| `robots.ts` / `robots.txt` | Robots directives |
| `manifest.ts` / `manifest.json` | Web app manifest |
| File | Purpose |
| ------------------------------- | --------------------------------------------- |
| `favicon.ico` | Favicon |
| `icon.png` / `icon.svg` | App icon |
| `apple-icon.png` | Apple app icon |
| `opengraph-image.png` | OG image |
| `twitter-image.png` | Twitter card image |
| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
| `robots.ts` / `robots.txt` | Robots directives |
| `manifest.ts` / `manifest.json` | Web app manifest |
## SEO Best Practice: Static Files Are Often Enough
@@ -108,6 +109,7 @@ app/
```
**Tips:**
- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)
- Static `title` and `description` in layout metadata is sufficient for most pages
- Only use dynamic `generateMetadata` when content varies per page
@@ -126,7 +128,7 @@ Generate dynamic Open Graph images using `next/og`.
```tsx
// Good
import { ImageResponse } from 'next/og'
import { ImageResponse } from 'next/og';
// Bad
// import { ImageResponse } from '@vercel/og'
@@ -137,11 +139,11 @@ import { ImageResponse } from 'next/og'
```tsx
// app/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { ImageResponse } from 'next/og';
export const alt = 'Site Name'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export const alt = 'Site Name';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default function Image() {
return new ImageResponse(
@@ -161,7 +163,7 @@ export default function Image() {
</div>
),
{ ...size }
)
);
}
```
@@ -169,17 +171,17 @@ export default function Image() {
```tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { ImageResponse } from 'next/og';
export const alt = 'Blog Post'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export const alt = 'Blog Post';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
type Props = { params: Promise<{ slug: string }> }
type Props = { params: Promise<{ slug: string }> };
export default async function Image({ params }: Props) {
const { slug } = await params
const post = await getPost(slug)
const { slug } = await params;
const post = await getPost(slug);
return new ImageResponse(
(
@@ -202,33 +204,26 @@ export default async function Image({ params }: Props) {
</div>
),
{ ...size }
)
);
}
```
## Custom Fonts
```tsx
import { ImageResponse } from 'next/og'
import { join } from 'path'
import { readFile } from 'fs/promises'
import { ImageResponse } from 'next/og';
import { join } from 'path';
import { readFile } from 'fs/promises';
export default async function Image() {
const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf')
const fontData = await readFile(fontPath)
const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf');
const fontData = await readFile(fontPath);
return new ImageResponse(
(
<div style={{ fontFamily: 'Inter', fontSize: 64 }}>
Custom Font Text
</div>
),
{
width: 1200,
height: 630,
fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
}
)
return new ImageResponse(<div style={{ fontFamily: 'Inter', fontSize: 64 }}>Custom Font Text</div>, {
width: 1200,
height: 630,
fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
});
}
```
@@ -240,6 +235,7 @@ export default async function Image() {
## Styling Notes
ImageResponse uses Flexbox layout:
- Use `display: 'flex'`
- No CSS Grid support
- Styles must be inline objects
@@ -250,22 +246,22 @@ Use `generateImageMetadata` for multiple images per route:
```tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { ImageResponse } from 'next/og';
export async function generateImageMetadata({ params }) {
const images = await getPostImages(params.slug)
const images = await getPostImages(params.slug);
return images.map((img, idx) => ({
id: idx,
alt: img.alt,
size: { width: 1200, height: 630 },
contentType: 'image/png',
}))
}));
}
export default async function Image({ params, id }) {
const images = await getPostImages(params.slug)
const image = images[id]
return new ImageResponse(/* ... */)
const images = await getPostImages(params.slug);
const image = images[id];
return new ImageResponse(/* ... */);
}
```
@@ -275,26 +271,22 @@ Use `generateSitemaps` for large sites:
```tsx
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import type { MetadataRoute } from 'next';
export async function generateSitemaps() {
// Return array of sitemap IDs
return [{ id: 0 }, { id: 1 }, { id: 2 }]
return [{ id: 0 }, { id: 1 }, { id: 2 }];
}
export default async function sitemap({
id,
}: {
id: number
}): Promise<MetadataRoute.Sitemap> {
const start = id * 50000
const end = start + 50000
const products = await getProducts(start, end)
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const start = id * 50000;
const end = start + 50000;
const products = await getProducts(start, end);
return products.map((product) => ({
url: `https://example.com/product/${product.id}`,
lastModified: product.updatedAt,
}))
}));
}
```
@@ -24,13 +24,7 @@ app/
```tsx
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
export default function RootLayout({ children, modal }: { children: React.ReactNode; modal: React.ReactNode }) {
return (
<html>
<body>
@@ -63,11 +57,7 @@ The `(.)` prefix intercepts routes at the same level.
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/modal';
export default async function PhotoModal({
params
}: {
params: Promise<{ id: string }>
}) {
export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const photo = await getPhoto(id);
@@ -83,11 +73,7 @@ export default async function PhotoModal({
```tsx
// app/photos/[id]/page.tsx
export default async function PhotoPage({
params
}: {
params: Promise<{ id: string }>
}) {
export default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const photo = await getPhoto(id);
@@ -127,11 +113,14 @@ export function Modal({ children }: { children: React.ReactNode }) {
}, [router]);
// Close on overlay click
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
if (e.target === overlayRef.current) {
router.back(); // Correct
}
}, [router]);
const handleOverlayClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === overlayRef.current) {
router.back(); // Correct
}
},
[router]
);
return (
<div
@@ -156,11 +145,13 @@ export function Modal({ children }: { children: React.ReactNode }) {
### Why NOT `router.push('/')` or `<Link href="/">`?
Using `push` or `Link` to "close" a modal:
1. Adds a new history entry (back button shows modal again)
2. Doesn't properly clear the intercepted route
3. Can cause the modal to flash or persist unexpectedly
`router.back()` correctly:
1. Removes the intercepted route from history
2. Returns to the previous page
3. Properly unmounts the modal
@@ -169,18 +160,19 @@ Using `push` or `Link` to "close" a modal:
Matchers match **route segments**, not filesystem paths:
| Matcher | Matches | Example |
|---------|---------|---------|
| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
| `(..)(..)` | Two levels up | Rarely used |
| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
| Matcher | Matches | Example |
| ---------- | ------------- | --------------------------------------------------------------------- |
| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
| `(..)(..)` | Two levels up | Rarely used |
| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
**Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment".
## Handling Hard Navigation
When users directly visit `/photos/123` (bookmark, refresh, shared link):
- The intercepting route is bypassed
- The full `photos/[id]/page.tsx` renders
- Modal doesn't appear (expected behavior)
@@ -230,6 +222,7 @@ app/
### 4. Intercepted Route Shows Wrong Content
Check your matcher:
- `(.)photos` intercepts `/photos` from the same route level
- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`
@@ -272,7 +265,7 @@ export default async function Gallery() {
return (
<div className="grid grid-cols-3 gap-4">
{photos.map(photo => (
{photos.map((photo) => (
<Link key={photo.id} href={`/photos/${photo.id}`}>
<img src={photo.thumbnail} alt={photo.title} />
</Link>
@@ -7,14 +7,14 @@ Create API endpoints with `route.ts` files.
```tsx
// app/api/users/route.ts
export async function GET() {
const users = await getUsers()
return Response.json(users)
const users = await getUsers();
return Response.json(users);
}
export async function POST(request: Request) {
const body = await request.json()
const user = await createUser(body)
return Response.json(user, { status: 201 })
const body = await request.json();
const user = await createUser(body);
return Response.json(user, { status: 201 });
}
```
@@ -60,11 +60,11 @@ Route handlers run in a **Server Component-like environment**:
```tsx
// Bad: This won't work - no React DOM in route handlers
import { renderToString } from 'react-dom/server'
import { renderToString } from 'react-dom/server';
export async function GET() {
const html = renderToString(<Component />) // Error!
return new Response(html)
const html = renderToString(<Component />); // Error!
return new Response(html);
}
```
@@ -72,18 +72,15 @@ export async function GET() {
```tsx
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const user = await getUser(id)
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await getUser(id);
if (!user) {
return Response.json({ error: 'Not found' }, { status: 404 })
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(user)
return Response.json(user);
}
```
@@ -92,17 +89,17 @@ export async function GET(
```tsx
export async function GET(request: Request) {
// URL and search params
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
// Headers
const authHeader = request.headers.get('authorization')
const authHeader = request.headers.get('authorization');
// Cookies (Next.js helper)
const cookieStore = await cookies()
const token = cookieStore.get('token')
const cookieStore = await cookies();
const token = cookieStore.get('token');
return Response.json({ query, token })
return Response.json({ query, token });
}
```
@@ -110,37 +107,37 @@ export async function GET(request: Request) {
```tsx
// JSON response
return Response.json({ data })
return Response.json({ data });
// With status
return Response.json({ error: 'Not found' }, { status: 404 })
return Response.json({ error: 'Not found' }, { status: 404 });
// With headers
return Response.json(data, {
headers: {
'Cache-Control': 'max-age=3600',
},
})
});
// Redirect
return Response.redirect(new URL('/login', request.url))
return Response.redirect(new URL('/login', request.url));
// Stream
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
})
});
```
## When to Use Route Handlers vs Server Actions
| Use Case | Route Handlers | Server Actions |
|----------|----------------|----------------|
| Form submissions | No | Yes |
| Data mutations from UI | No | Yes |
| Third-party webhooks | Yes | No |
| External API consumption | Yes | No |
| Public REST API | Yes | No |
| File uploads | Both work | Both work |
| Use Case | Route Handlers | Server Actions |
| ------------------------ | -------------- | -------------- |
| Form submissions | No | Yes |
| Data mutations from UI | No | Yes |
| Third-party webhooks | Yes | No |
| External API consumption | Yes | No |
| Public REST API | Yes | No |
| File uploads | Both work | Both work |
**Prefer Server Actions** for mutations triggered from your UI.
**Use Route Handlers** for external integrations and public APIs.
@@ -12,33 +12,33 @@ Client components **cannot** be async functions. Only Server Components can be a
```tsx
// Bad: async client component
'use client'
'use client';
export default async function UserProfile() {
const user = await getUser() // Cannot await in client component
return <div>{user.name}</div>
const user = await getUser(); // Cannot await in client component
return <div>{user.name}</div>;
}
// Good: Remove async, fetch data in parent server component
// page.tsx (server component - no 'use client')
export default async function Page() {
const user = await getUser()
return <UserProfile user={user} />
const user = await getUser();
return <UserProfile user={user} />;
}
// UserProfile.tsx (client component)
'use client'
('use client');
export function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>
return <div>{user.name}</div>;
}
```
```tsx
// Bad: async arrow function client component
'use client'
'use client';
const Dashboard = async () => {
const data = await fetchDashboard()
return <div>{data}</div>
}
const data = await fetchDashboard();
return <div>{data}</div>;
};
// Good: Fetch in server component, pass data down
```
@@ -48,6 +48,7 @@ const Dashboard = async () => {
Props passed from Server → Client must be JSON-serializable.
**Detect:** Server component passes these to a client component:
- Functions (except Server Actions with `'use server'`)
- `Date` objects
- `Map`, `Set`, `WeakMap`, `WeakSet`
@@ -59,16 +60,16 @@ Props passed from Server → Client must be JSON-serializable.
// Bad: Function prop
// page.tsx (server)
export default function Page() {
const handleClick = () => console.log('clicked')
return <ClientButton onClick={handleClick} />
const handleClick = () => console.log('clicked');
return <ClientButton onClick={handleClick} />;
}
// Good: Define function inside client component
// ClientButton.tsx
'use client'
('use client');
export function ClientButton() {
const handleClick = () => console.log('clicked')
return <button onClick={handleClick}>Click</button>
const handleClick = () => console.log('clicked');
return <button onClick={handleClick}>Click</button>;
}
```
@@ -76,28 +77,28 @@ export function ClientButton() {
// Bad: Date object (silently becomes string, then crashes)
// page.tsx (server)
export default async function Page() {
const post = await getPost()
return <PostCard createdAt={post.createdAt} /> // Date object
const post = await getPost();
return <PostCard createdAt={post.createdAt} />; // Date object
}
// PostCard.tsx (client) - will crash on .getFullYear()
'use client'
('use client');
export function PostCard({ createdAt }: { createdAt: Date }) {
return <span>{createdAt.getFullYear()}</span> // Runtime error!
return <span>{createdAt.getFullYear()}</span>; // Runtime error!
}
// Good: Serialize to string on server
// page.tsx (server)
export default async function Page() {
const post = await getPost()
return <PostCard createdAt={post.createdAt.toISOString()} />
const post = await getPost();
return <PostCard createdAt={post.createdAt.toISOString()} />;
}
// PostCard.tsx (client)
'use client'
('use client');
export function PostCard({ createdAt }: { createdAt: string }) {
const date = new Date(createdAt)
return <span>{date.getFullYear()}</span>
const date = new Date(createdAt);
return <span>{date.getFullYear()}</span>;
}
```
@@ -127,33 +128,33 @@ Functions marked with `'use server'` CAN be passed to client components.
```tsx
// Valid: Server Action can be passed
// actions.ts
'use server'
'use server';
export async function submitForm(formData: FormData) {
// server-side logic
}
// page.tsx (server)
import { submitForm } from './actions'
import { submitForm } from './actions';
export default function Page() {
return <ClientForm onSubmit={submitForm} /> // OK!
return <ClientForm onSubmit={submitForm} />; // OK!
}
// ClientForm.tsx (client)
'use client'
('use client');
export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
return <form action={onSubmit}>...</form>
return <form action={onSubmit}>...</form>;
}
```
## Quick Reference
| Pattern | Valid? | Fix |
|---------|--------|-----|
| `'use client'` + `async function` | No | Fetch in server parent, pass data |
| Pass `() => {}` to client | No | Define in client or use server action |
| Pass `new Date()` to client | No | Use `.toISOString()` |
| Pass `new Map()` to client | No | Convert to object/array |
| Pass class instance to client | No | Pass plain object |
| Pass server action to client | Yes | - |
| Pass `string/number/boolean` | Yes | - |
| Pass plain object/array | Yes | - |
| Pattern | Valid? | Fix |
| --------------------------------- | ------ | ------------------------------------- |
| `'use client'` + `async function` | No | Fetch in server parent, pass data |
| Pass `() => {}` to client | No | Define in client or use server action |
| Pass `new Date()` to client | No | Use `.toISOString()` |
| Pass `new Map()` to client | No | Convert to object/array |
| Pass class instance to client | No | Pass plain object |
| Pass server action to client | Yes | - |
| Pass `string/number/boolean` | Yes | - |
| Pass plain object/array | Yes | - |
@@ -32,6 +32,7 @@ export const runtime = 'edge'
## Detection
**Before adding `runtime = 'edge'`**, check:
1. Does the project already use Edge runtime?
2. Is there a specific latency requirement?
3. Are all dependencies Edge-compatible?
+16 -20
View File
@@ -8,12 +8,12 @@ Always use `next/script` instead of native `<script>` tags for better performanc
```tsx
// Bad: Native script tag
<script src="https://example.com/script.js"></script>
<script src="https://example.com/script.js"></script>;
// Good: Next.js Script component
import Script from 'next/script'
import Script from 'next/script';
<Script src="https://example.com/script.js" />
<Script src="https://example.com/script.js" />;
```
## Inline Scripts Need ID
@@ -100,7 +100,7 @@ export default function Layout({ children }) {
## Google Tag Manager
```tsx
import { GoogleTagManager } from '@next/third-parties/google'
import { GoogleTagManager } from '@next/third-parties/google';
export default function Layout({ children }) {
return (
@@ -108,7 +108,7 @@ export default function Layout({ children }) {
<GoogleTagManager gtmId="GTM-XXXXX" />
<body>{children}</body>
</html>
)
);
}
```
@@ -116,26 +116,22 @@ export default function Layout({ children }) {
```tsx
// YouTube embed
import { YouTubeEmbed } from '@next/third-parties/google'
import { YouTubeEmbed } from '@next/third-parties/google';
<YouTubeEmbed videoid="dQw4w9WgXcQ" />
<YouTubeEmbed videoid="dQw4w9WgXcQ" />;
// Google Maps
import { GoogleMapsEmbed } from '@next/third-parties/google'
import { GoogleMapsEmbed } from '@next/third-parties/google';
<GoogleMapsEmbed
apiKey="YOUR_API_KEY"
mode="place"
q="Brooklyn+Bridge,New+York,NY"
/>
<GoogleMapsEmbed apiKey="YOUR_API_KEY" mode="place" q="Brooklyn+Bridge,New+York,NY" />;
```
## Quick Reference
| Pattern | Issue | Fix |
|---------|-------|-----|
| `<script src="...">` | No optimization | Use `next/script` |
| `<Script>` without id | Can't track inline scripts | Add `id` attribute |
| `<Script>` inside `<Head>` | Wrong placement | Move outside Head |
| Inline GA/GTM scripts | No optimization | Use `@next/third-parties` |
| `strategy="beforeInteractive"` outside layout | Won't work | Only use in root layout |
| Pattern | Issue | Fix |
| --------------------------------------------- | -------------------------- | ------------------------- |
| `<script src="...">` | No optimization | Use `next/script` |
| `<Script>` without id | Can't track inline scripts | Add `id` attribute |
| `<Script>` inside `<Head>` | Wrong placement | Move outside Head |
| Inline GA/GTM scripts | No optimization | Use `@next/third-parties` |
| `strategy="beforeInteractive"` outside layout | Won't work | Only use in root layout |
@@ -77,12 +77,12 @@ services:
web:
build: .
ports:
- "3000:3000"
- '3000:3000'
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']
interval: 30s
timeout: 10s
retries: 3
@@ -95,16 +95,18 @@ For traditional server deployments:
```js
// ecosystem.config.js
module.exports = {
apps: [{
name: 'nextjs',
script: '.next/standalone/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
apps: [
{
name: 'nextjs',
script: '.next/standalone/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
},
}],
],
};
```
@@ -168,11 +170,7 @@ module.exports = class CacheHandler {
// Set TTL based on revalidate option
if (ctx?.revalidate) {
await redis.setex(
CACHE_PREFIX + key,
ctx.revalidate,
JSON.stringify(cacheData)
);
await redis.setex(CACHE_PREFIX + key, ctx.revalidate, JSON.stringify(cacheData));
} else {
await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));
}
@@ -197,10 +195,12 @@ const BUCKET = process.env.CACHE_BUCKET;
module.exports = class CacheHandler {
async get(key) {
try {
const response = await s3.send(new GetObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
}));
const response = await s3.send(
new GetObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
})
);
const body = await response.Body.transformToString();
return JSON.parse(body);
} catch (err) {
@@ -210,32 +210,34 @@ module.exports = class CacheHandler {
}
async set(key, data, ctx) {
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
Body: JSON.stringify({
value: data,
lastModified: Date.now(),
}),
ContentType: 'application/json',
}));
await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
Body: JSON.stringify({
value: data,
lastModified: Date.now(),
}),
ContentType: 'application/json',
})
);
}
};
```
## What Works vs What Needs Setup
| Feature | Single Instance | Multi-Instance | Notes |
|---------|----------------|----------------|-------|
| SSR | Yes | Yes | No special setup |
| SSG | Yes | Yes | Built at deploy time |
| ISR | Yes | Needs cache handler | Filesystem cache breaks |
| Image Optimization | Yes | Yes | CPU-intensive, consider CDN |
| Middleware | Yes | Yes | Runs on Node.js |
| Edge Runtime | Limited | Limited | Some features Node-only |
| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |
| `next/font` | Yes | Yes | Fonts bundled at build |
| Draft Mode | Yes | Yes | Cookie-based |
| Feature | Single Instance | Multi-Instance | Notes |
| -------------------- | --------------- | ------------------- | --------------------------- |
| SSR | Yes | Yes | No special setup |
| SSG | Yes | Yes | Built at deploy time |
| ISR | Yes | Needs cache handler | Filesystem cache breaks |
| Image Optimization | Yes | Yes | CPU-intensive, consider CDN |
| Middleware | Yes | Yes | Runs on Node.js |
| Edge Runtime | Limited | Limited | Some features Node-only |
| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |
| `next/font` | Yes | Yes | Fonts bundled at build |
| Draft Mode | Yes | Yes | Cookie-based |
## Image Optimization
@@ -244,6 +246,7 @@ Next.js Image Optimization works out of the box but is CPU-intensive.
### Option 1: Built-in (Simple)
Works automatically, but consider:
- Set `deviceSizes` and `imageSizes` in config to limit variants
- Use `minimumCacheTTL` to reduce regeneration
@@ -317,6 +320,7 @@ npx @opennextjs/aws build
```
Supports:
- AWS Lambda + CloudFront
- Cloudflare Workers
- Netlify Functions
@@ -8,27 +8,27 @@ Always requires Suspense boundary in static routes. Without it, the entire page
```tsx
// Bad: Entire page becomes CSR
'use client'
'use client';
import { useSearchParams } from 'next/navigation'
import { useSearchParams } from 'next/navigation';
export default function SearchBar() {
const searchParams = useSearchParams()
return <div>Query: {searchParams.get('q')}</div>
const searchParams = useSearchParams();
return <div>Query: {searchParams.get('q')}</div>;
}
```
```tsx
// Good: Wrap in Suspense
import { Suspense } from 'react'
import SearchBar from './search-bar'
import { Suspense } from 'react';
import SearchBar from './search-bar';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchBar />
</Suspense>
)
);
}
```
@@ -39,12 +39,12 @@ Requires Suspense boundary when route has dynamic parameters.
```tsx
// In dynamic route [slug]
// Bad: No Suspense
'use client'
import { usePathname } from 'next/navigation'
'use client';
import { usePathname } from 'next/navigation';
export function Breadcrumb() {
const pathname = usePathname()
return <nav>{pathname}</nav>
const pathname = usePathname();
return <nav>{pathname}</nav>;
}
```
@@ -59,9 +59,9 @@ If you use `generateStaticParams`, Suspense is optional.
## Quick Reference
| Hook | Suspense Required |
|------|-------------------|
| `useSearchParams()` | Yes |
| `usePathname()` | Yes (dynamic routes) |
| `useParams()` | No |
| `useRouter()` | No |
| Hook | Suspense Required |
| ------------------- | -------------------- |
| `useSearchParams()` | Yes |
| `usePathname()` | Yes (dynamic routes) |
| `useParams()` | No |
| `useRouter()` | No |
+5 -4
View File
@@ -21,6 +21,7 @@ You are the **Antigravity Consistency Analyst**. Your role is to identify incons
## Task
### Goal
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit-tasks` has successfully produced a complete `tasks.md`.
## Operating Constraints
@@ -135,16 +136,16 @@ Output a Markdown report (no file writes) with the following structure:
## Specification Analysis Report
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------|
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
| ID | Category | Severity | Location(s) | Summary | Recommendation |
| --- | ----------- | -------- | ---------------- | ---------------------------- | ------------------------------------ |
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
(Add one row per finding; generate stable IDs prefixed by category initial.)
**Coverage Summary Table:**
| Requirement Key | Has Task? | Task IDs | Notes |
|-----------------|-----------|----------|-------|
| --------------- | --------- | -------- | ----- |
**Constitution Alignment Issues:** (if any)
+80 -75
View File
@@ -26,119 +26,124 @@ Auto-detect available tools, run them, and aggregate results into a prioritized
### Execution Steps
1. **Detect Project Type and Tools**:
```bash
# Check for config files
ls -la | grep -E "(package.json|pyproject.toml|go.mod|Cargo.toml|pom.xml)"
# Check for linter configs
ls -la | grep -E "(eslint|prettier|pylint|golangci|rustfmt)"
```
| Config | Tools to Run |
|--------|-------------|
| `package.json` | ESLint, TypeScript, npm audit |
| `pyproject.toml` | Pylint/Ruff, mypy, bandit |
| `go.mod` | golangci-lint, go vet |
| `Cargo.toml` | clippy, cargo audit |
| `pom.xml` | SpotBugs, PMD |
| Config | Tools to Run |
| ---------------- | ----------------------------- |
| `package.json` | ESLint, TypeScript, npm audit |
| `pyproject.toml` | Pylint/Ruff, mypy, bandit |
| `go.mod` | golangci-lint, go vet |
| `Cargo.toml` | clippy, cargo audit |
| `pom.xml` | SpotBugs, PMD |
2. **Run Linting**:
| Stack | Command |
|-------|---------|
| Node/TS | `npx eslint . --format json 2>/dev/null` |
| Python | `ruff check . --output-format json 2>/dev/null || pylint --output-format=json **/*.py` |
| Go | `golangci-lint run --out-format json` |
| Rust | `cargo clippy --message-format=json` |
| Stack | Command |
| ------- | ---------------------------------------------- | --- | ------------------------------------- |
| Node/TS | `npx eslint . --format json 2>/dev/null` |
| Python | `ruff check . --output-format json 2>/dev/null | | pylint --output-format=json \*_/_.py` |
| Go | `golangci-lint run --out-format json` |
| Rust | `cargo clippy --message-format=json` |
3. **Run Type Checking**:
| Stack | Command |
|-------|---------|
| TypeScript | `npx tsc --noEmit 2>&1` |
| Python | `mypy . --no-error-summary 2>&1` |
| Go | `go build ./... 2>&1` (types are built-in) |
| Stack | Command |
| ---------- | ------------------------------------------ |
| TypeScript | `npx tsc --noEmit 2>&1` |
| Python | `mypy . --no-error-summary 2>&1` |
| Go | `go build ./... 2>&1` (types are built-in) |
4. **Run Security Scanning**:
| Stack | Command |
|-------|---------|
| Node | `npm audit --json` |
| Python | `bandit -r . -f json 2>/dev/null || safety check --json` |
| Go | `govulncheck ./... 2>&1` |
| Rust | `cargo audit --json` |
| Stack | Command |
| ------ | -------------------------------- | --- | -------------------- |
| Node | `npm audit --json` |
| Python | `bandit -r . -f json 2>/dev/null | | safety check --json` |
| Go | `govulncheck ./... 2>&1` |
| Rust | `cargo audit --json` |
5. **Aggregate and Prioritize**:
| Category | Priority |
|----------|----------|
| Security (Critical/High) | 🔴 P1 |
| Type Errors | 🟠 P2 |
| Security (Medium/Low) | 🟡 P3 |
| Lint Errors | 🟡 P3 |
| Lint Warnings | 🟢 P4 |
| Style Issues | ⚪ P5 |
| Category | Priority |
| ------------------------ | -------- |
| Security (Critical/High) | 🔴 P1 |
| Type Errors | 🟠 P2 |
| Security (Medium/Low) | 🟡 P3 |
| Lint Errors | 🟡 P3 |
| Lint Warnings | 🟢 P4 |
| Style Issues | ⚪ P5 |
6. **Generate Report**:
```markdown
````markdown
# Static Analysis Report
**Date**: [timestamp]
**Project**: [name from package.json/pyproject.toml]
**Status**: CLEAN | ISSUES FOUND
## Tools Run
| Tool | Status | Issues |
|------|--------|--------|
| ESLint | ✅ | 12 |
| TypeScript | ✅ | 3 |
| npm audit | ⚠️ | 2 vulnerabilities |
| Tool | Status | Issues |
| ---------- | ------ | ----------------- |
| ESLint | ✅ | 12 |
| TypeScript | ✅ | 3 |
| npm audit | ⚠️ | 2 vulnerabilities |
## Summary by Priority
| Priority | Count |
|----------|-------|
| 🔴 P1 Critical | X |
| 🟠 P2 High | X |
| 🟡 P3 Medium | X |
| 🟢 P4 Low | X |
| Priority | Count |
| -------------- | ----- |
| 🔴 P1 Critical | X |
| 🟠 P2 High | X |
| 🟡 P3 Medium | X |
| 🟢 P4 Low | X |
## Issues
### 🔴 P1: Security Vulnerabilities
| Package | Severity | Issue | Fix |
|---------|----------|-------|-----|
| lodash | HIGH | Prototype Pollution | Upgrade to 4.17.21 |
| Package | Severity | Issue | Fix |
| ------- | -------- | ------------------- | ------------------ |
| lodash | HIGH | Prototype Pollution | Upgrade to 4.17.21 |
### 🟠 P2: Type Errors
| File | Line | Error |
|------|------|-------|
| src/api.ts | 45 | Type 'string' is not assignable to type 'number' |
| File | Line | Error |
| ---------- | ---- | ------------------------------------------------ |
| src/api.ts | 45 | Type 'string' is not assignable to type 'number' |
### 🟡 P3: Lint Issues
| File | Line | Rule | Message |
|------|------|------|---------|
| src/utils.ts | 12 | no-unused-vars | 'foo' is defined but never used |
| File | Line | Rule | Message |
| ------------ | ---- | -------------- | ------------------------------- |
| src/utils.ts | 12 | no-unused-vars | 'foo' is defined but never used |
## Quick Fixes
```bash
# Fix security issues
npm audit fix
# Auto-fix lint issues
npx eslint . --fix
```
````
## Recommendations
1. **Immediate**: Fix P1 security issues
2. **Before merge**: Fix P2 type errors
3. **Tech debt**: Address P3/P4 lint issues
```
```
7. **Output**:
@@ -6,16 +6,16 @@
**Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
<!--
<!--
============================================================================
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
The /speckit-checklist command MUST replace these with actual items based on:
- User's specific checklist request
- Feature requirements from spec.md
- Technical context from plan.md
- Implementation details from tasks.md
DO NOT keep these sample items in the generated checklist file.
============================================================================
-->
+58 -58
View File
@@ -4,7 +4,7 @@ description: Identify underspecified areas in the current feature spec by asking
version: 1.0.0
depends-on:
- speckit-specify
handoffs:
handoffs:
- label: Build Technical Plan
agent: speckit-plan
prompt: Create a plan for the spec. I am building with...
@@ -96,69 +96,69 @@ Execution steps:
- Information is better deferred to planning phase (note internally)
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
- Maximum of 10 total questions across the whole session.
- Each question must be answerable with EITHER:
- A short multiplechoice selection (25 distinct, mutually exclusive options), OR
- A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words").
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
- Maximum of 10 total questions across the whole session.
- Each question must be answerable with EITHER:
- A short multiplechoice selection (25 distinct, mutually exclusive options), OR
- A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words").
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
- If more than 5 categories remain unresolved, select the top 5 by (Impact \* Uncertainty) heuristic.
4. Sequential questioning loop (interactive):
- Present EXACTLY ONE question at a time.
- For multiplechoice questions:
- **Analyze all options** and determine the **most suitable option** based on:
- Best practices for the project type
- Common patterns in similar implementations
- Risk reduction (security, performance, maintainability)
- Alignment with any explicit project goals or constraints visible in the spec
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
- Format as: `**Recommended:** Option [X] - <reasoning>`
- Then render all options as a Markdown table:
- Present EXACTLY ONE question at a time.
- For multiplechoice questions:
- **Analyze all options** and determine the **most suitable option** based on:
- Best practices for the project type
- Common patterns in similar implementations
- Risk reduction (security, performance, maintainability)
- Alignment with any explicit project goals or constraints visible in the spec
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
- Format as: `**Recommended:** Option [X] - <reasoning>`
- Then render all options as a Markdown table:
| Option | Description |
|--------|-------------|
| A | <Option A description> |
| B | <Option B description> |
| C | <Option C description> (add D/E as needed up to 5) |
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
| Option | Description |
| ------ | --------------------------------------------------------------------------------------------------- |
| A | <Option A description> |
| B | <Option B description> |
| C | <Option C description> (add D/E as needed up to 5) |
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
- For shortanswer style (no meaningful discrete options):
- Provide your **suggested answer** based on best practices and context.
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
- After the user answers:
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
- Stop asking further questions when:
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
- User signals completion ("done", "good", "no more"), OR
- You reach 5 asked questions.
- Never reveal future queued questions in advance.
- If no valid questions exist at start, immediately report no critical ambiguities.
- For shortanswer style (no meaningful discrete options):
- Provide your **suggested answer** based on best practices and context.
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
- After the user answers:
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
- Stop asking further questions when:
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
- User signals completion ("done", "good", "no more"), OR
- You reach 5 asked questions.
- Never reveal future queued questions in advance.
- If no valid questions exist at start, immediately report no critical ambiguities.
5. Integration after EACH accepted answer (incremental update approach):
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
- For the first integrated answer in this session:
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
- Then immediately apply the clarification to the most appropriate section(s):
- Functional ambiguity → Update or add a bullet in Functional Requirements.
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
- Keep each inserted clarification minimal and testable (avoid narrative drift).
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
- For the first integrated answer in this session:
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
- Then immediately apply the clarification to the most appropriate section(s):
- Functional ambiguity → Update or add a bullet in Functional Requirements.
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
- Keep each inserted clarification minimal and testable (avoid narrative drift).
6. Validation (performed after EACH write plus final pass):
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
+15 -10
View File
@@ -31,10 +31,12 @@ Compare two versions of a specification artifact and produce a structured diff r
- If no arguments: Use `check-prerequisites.sh` to find current feature's spec.md and compare with HEAD
2. **Load Files**:
```bash
# For git comparison
git show HEAD:<relative-path> > /tmp/old_version.md
```
- Read both versions into memory
3. **Semantic Diff Analysis**:
@@ -45,26 +47,29 @@ Compare two versions of a specification artifact and produce a structured diff r
- **Moved**: Reorganized content (same meaning, different location)
4. **Generate Report**:
```markdown
# Diff Report: [filename]
**Compared**: [version A] → [version B]
**Date**: [timestamp]
## Summary
- X additions, Y removals, Z modifications
## Changes by Section
### [Section Name]
| Type | Content | Impact |
|------|---------|--------|
| + Added | [new text] | [what this means] |
| - Removed | [old text] | [what this means] |
| Type | Content | Impact |
| ---------- | ------------------ | ----------------- |
| + Added | [new text] | [what this means] |
| - Removed | [old text] | [what this means] |
| ~ Modified | [before] → [after] | [what this means] |
## Risk Assessment
- Breaking changes: [list any]
- Scope changes: [list any]
```
+29 -29
View File
@@ -53,7 +53,7 @@ If a file is critical, complex, or has high dependencies (>2 affected files):
4. **SWITCH** the imports in the consuming files one by one.
5. **ANNOUNCE**: "Applying Strangler Pattern to avoid regression."
*Benefit: If it breaks, we simply revert the import, not the whole logic.*
_Benefit: If it breaks, we simply revert the import, not the whole logic._
### Protocol 3: Reproduction Script First (TDD)
@@ -136,12 +136,12 @@ At the start of execution and after every 3 modifications:
git rev-parse --git-dir 2>/dev/null
```
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc* exists → create/verify .eslintignore
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc* exists → create/verify .prettierignore
- Check if Dockerfile\* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc\* exists → create/verify .eslintignore
- Check if eslint.config.\* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc\* exists → create/verify .prettierignore
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
- Check if terraform files (*.tf) exist → create/verify .terraformignore
- Check if terraform files (\*.tf) exist → create/verify .terraformignore
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
@@ -179,35 +179,35 @@ At the start of execution and after every 3 modifications:
7. **Execute implementation following the task plan with Ironclad Protocols**:
**For EACH task**, follow this sequence:
a. **Blast Radius Analysis (Protocol 1)**:
- Identify all files that will be modified
- Run `grep` to find all dependents
- Report the blast radius
- Identify all files that will be modified
- Run `grep` to find all dependents
- Report the blast radius
b. **Strategy Decision**:
- If LOW risk (≤2 affected files): Proceed with inline modification
- If MEDIUM/HIGH risk (>2 files): Apply Strangler Pattern (Protocol 2)
- If LOW risk (≤2 affected files): Proceed with inline modification
- If MEDIUM/HIGH risk (>2 files): Apply Strangler Pattern (Protocol 2)
c. **Reproduction Script (Protocol 3)**:
- Create `repro_task_[ID].ts` that demonstrates expected behavior
- Run it to confirm current state (should fail for new features, or fail for bugs)
- Create `repro_task_[ID].ts` that demonstrates expected behavior
- Run it to confirm current state (should fail for new features, or fail for bugs)
d. **Implementation**:
- Execute the task according to plan
- **Phase-by-phase execution**: Complete each phase before moving to the next
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
- **File-based coordination**: Tasks affecting the same files must run sequentially
- Execute the task according to plan
- **Phase-by-phase execution**: Complete each phase before moving to the next
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
- **File-based coordination**: Tasks affecting the same files must run sequentially
e. **Verification**:
- Run the reproduction script again (should now pass)
- Run existing tests to ensure no regression
- If any test fails: **STOP** and report the regression
- Run the reproduction script again (should now pass)
- Run existing tests to ensure no regression
- If any test fails: **STOP** and report the regression
f. **Cleanup**:
- Delete temporary repro scripts OR convert to permanent tests
- Mark task as complete `[X]` in tasks.md
- Delete temporary repro scripts OR convert to permanent tests
- Mark task as complete `[X]` in tasks.md
8. **Progress tracking and error handling**:
- Report progress after each completed task with this format:
+23 -11
View File
@@ -31,10 +31,11 @@ Analyze an existing codebase and generate speckit artifacts (spec.md, plan.md, t
- `--depth <n>`: Analysis depth (1=overview, 2=detailed, 3=exhaustive)
2. **Codebase Discovery**:
```bash
# Get project structure
tree -L 3 --dirsfirst -I 'node_modules|.git|dist|build' > /tmp/structure.txt
# Find key files
find . -name "*.md" -o -name "package.json" -o -name "*.config.*" | head -50
```
@@ -47,48 +48,59 @@ Analyze an existing codebase and generate speckit artifacts (spec.md, plan.md, t
- Map API endpoints (if applicable)
4. **Generate spec.md** (reverse-engineered):
```markdown
# [Feature Name] - Specification (Migrated)
> This specification was auto-generated from existing code.
> Review and refine before using for future development.
## Overview
[Inferred from README, comments, and code structure]
## Functional Requirements
[Extracted from existing functionality]
## Key Entities
[From data models, schemas, types]
```
5. **Generate plan.md** (reverse-engineered):
```markdown
# [Feature Name] - Technical Plan (Migrated)
## Current Architecture
[Documented from codebase analysis]
## Technology Stack
[From package.json, imports, configs]
## Component Map
[Directory → responsibility mapping]
```
6. **Generate tasks.md** (completion status):
```markdown
# [Feature Name] - Tasks (Migrated)
All tasks marked [x] represent existing implemented functionality.
Tasks marked [ ] are inferred gaps or TODOs found in code.
## Existing Implementation
- [x] [Component A] - Implemented in `src/componentA/`
- [x] [Component B] - Implemented in `src/componentB/`
## Identified Gaps
- [ ] [Missing tests for X]
- [ ] [TODO comment at Y]
```
+2 -2
View File
@@ -4,7 +4,7 @@ description: Execute the implementation planning workflow using the plan templat
version: 1.0.0
depends-on:
- speckit-specify
handoffs:
handoffs:
- label: Create Tasks
agent: speckit-tasks
prompt: Break the plan into tasks
@@ -91,7 +91,7 @@ You are the **Antigravity System Architect**. Your role is to bridge the gap bet
- Add only new technology from current plan
- Preserve manual additions between markers
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
**Output**: data-model.md, /contracts/\*, quickstart.md, agent-specific file
## Key rules
@@ -29,7 +29,7 @@
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
[Gates determined based on constitution file]
@@ -48,6 +48,7 @@ specs/[###-feature]/
```
### Source Code (repository root)
<!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with
@@ -98,7 +99,7 @@ directories captured above]
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
| Violation | Why Needed | Simpler Alternative Rejected Because |
| -------------------------- | ------------------ | ------------------------------------ |
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
+42 -34
View File
@@ -33,7 +33,7 @@ Review code changes and provide structured feedback with severity levels.
```bash
# Get staged changes
git diff --cached --name-only
# Get branch changes
git diff main...HEAD --name-only
```
@@ -44,14 +44,14 @@ Review code changes and provide structured feedback with severity levels.
3. **Review Categories**:
| Category | What to Check |
|----------|--------------|
| **Correctness** | Logic errors, off-by-one, null handling |
| **Security** | SQL injection, XSS, secrets in code |
| **Performance** | N+1 queries, unnecessary loops, memory leaks |
| **Maintainability** | Complexity, duplication, naming |
| **Best Practices** | Error handling, logging, typing |
| **Style** | Consistency, formatting (if no linter) |
| Category | What to Check |
| ------------------- | -------------------------------------------- |
| **Correctness** | Logic errors, off-by-one, null handling |
| **Security** | SQL injection, XSS, secrets in code |
| **Performance** | N+1 queries, unnecessary loops, memory leaks |
| **Maintainability** | Complexity, duplication, naming |
| **Best Practices** | Error handling, logging, typing |
| **Style** | Consistency, formatting (if no linter) |
4. **Analyze Each File**:
For each file, check:
@@ -64,63 +64,71 @@ Review code changes and provide structured feedback with severity levels.
5. **Severity Levels**:
| Level | Meaning | Block Merge? |
|-------|---------|--------------|
| 🔴 CRITICAL | Security issue, data loss risk | Yes |
| 🟠 HIGH | Bug, logic error | Yes |
| 🟡 MEDIUM | Code smell, maintainability | Maybe |
| 🟢 LOW | Style, minor improvement | No |
| 💡 SUGGESTION | Nice-to-have, optional | No |
| Level | Meaning | Block Merge? |
| ------------- | ------------------------------ | ------------ |
| 🔴 CRITICAL | Security issue, data loss risk | Yes |
| 🟠 HIGH | Bug, logic error | Yes |
| 🟡 MEDIUM | Code smell, maintainability | Maybe |
| 🟢 LOW | Style, minor improvement | No |
| 💡 SUGGESTION | Nice-to-have, optional | No |
6. **Generate Review Report**:
```markdown
````markdown
# Code Review Report
**Date**: [timestamp]
**Scope**: [files reviewed]
**Overall**: APPROVE | REQUEST CHANGES | NEEDS DISCUSSION
## Summary
| Severity | Count |
|----------|-------|
| 🔴 Critical | X |
| 🟠 High | X |
| 🟡 Medium | X |
| 🟢 Low | X |
| 💡 Suggestions | X |
| Severity | Count |
| -------------- | ----- |
| 🔴 Critical | X |
| 🟠 High | X |
| 🟡 Medium | X |
| 🟢 Low | X |
| 💡 Suggestions | X |
## Findings
### 🔴 CRITICAL: SQL Injection Risk
**File**: `src/db/queries.ts:45`
**Code**:
```typescript
const query = `SELECT * FROM users WHERE id = ${userId}`;
```
````
**Issue**: User input directly concatenated into SQL query
**Fix**: Use parameterized queries:
```typescript
const query = 'SELECT * FROM users WHERE id = $1';
await db.query(query, [userId]);
```
### 🟡 MEDIUM: Complex Function
**File**: `src/auth/handler.ts:120`
**Issue**: Function has cyclomatic complexity of 15
**Suggestion**: Extract into smaller functions
## What's Good
- Clear naming conventions
- Good test coverage
- Proper TypeScript types
## Recommended Actions
1. **Must fix before merge**: [critical/high items]
2. **Should address**: [medium items]
3. **Consider for later**: [low/suggestions]
```
```
7. **Output**:
@@ -131,8 +131,8 @@ Check LCBP3-DMS-specific file handling per ADR-016:
## Severity Classification
| Severity | Description | Response |
| -------------- | ----------------------------------------------------- | ----------------------- |
| Severity | Description | Response |
| --------------- | ----------------------------------------------------- | ----------------------- |
| 🔴 **Critical** | Exploitable vulnerability, data exposure, auth bypass | Immediate fix required |
| 🟠 **High** | Missing security control, potential escalation path | Fix before next release |
| 🟡 **Medium** | Best practice violation, defense-in-depth gap | Plan fix in sprint |
@@ -151,8 +151,8 @@ Generate a structured report:
## Summary
| Severity | Count |
| ---------- | ----- |
| Severity | Count |
| ----------- | ----- |
| 🔴 Critical | X |
| 🟠 High | X |
| 🟡 Medium | X |
@@ -179,8 +179,8 @@ Generate a structured report:
| Module | Controller | Guard? | Policies? | Level |
| ------ | --------------- | ------ | --------- | ------------ |
| auth | AuthController | ✅ | ✅ | N/A (public) |
| users | UsersController | ✅ | ✅ | L1-L4 |
| auth | AuthController | ✅ | ✅ | N/A (public) |
| users | UsersController | ✅ | ✅ | L1-L4 |
| ... | ... | ... | ... | ... |
## Recommendations Priority
@@ -5,13 +5,13 @@
**Status**: Draft
**Input**: User description: "$ARGUMENTS"
## User Scenarios & Testing *(mandatory)*
## User Scenarios & Testing _(mandatory)_
<!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be:
- Developed independently
@@ -75,7 +75,7 @@
- What happens when [boundary condition]?
- How does system handle [error scenario]?
## Requirements *(mandatory)*
## Requirements _(mandatory)_
<!--
ACTION REQUIRED: The content in this section represents placeholders.
@@ -85,22 +85,22 @@
### Functional Requirements
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
*Example of marking unclear requirements:*
_Example of marking unclear requirements:_
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
### Key Entities *(include if feature involves data)*
### Key Entities _(include if feature involves data)_
- **[Entity 1]**: [What it represents, key attributes without implementation]
- **[Entity 2]**: [What it represents, relationships to other entities]
## Success Criteria *(mandatory)*
## Success Criteria _(mandatory)_
<!--
ACTION REQUIRED: Define measurable success criteria.
+27 -23
View File
@@ -26,6 +26,7 @@ Generate a dashboard view of all features and their completion status.
### Execution Steps
1. **Discover Features**:
```bash
# Find all feature directories
find .specify/features -maxdepth 1 -type d 2>/dev/null || echo "No features found"
@@ -33,14 +34,15 @@ Generate a dashboard view of all features and their completion status.
2. **For Each Feature, Gather Metrics**:
| Artifact | Check | Metric |
|----------|-------|--------|
| spec.md | Exists? | Has [NEEDS CLARIFICATION]? |
| plan.md | Exists? | All sections complete? |
| tasks.md | Exists? | Count [x] vs [ ] vs [/] |
| checklists/*.md | All items checked? | Checklist completion % |
| Artifact | Check | Metric |
| ---------------- | ------------------ | -------------------------- |
| spec.md | Exists? | Has [NEEDS CLARIFICATION]? |
| plan.md | Exists? | All sections complete? |
| tasks.md | Exists? | Count [x] vs [ ] vs [/] |
| checklists/\*.md | All items checked? | Checklist completion % |
3. **Calculate Completion**:
```
Phase 1 (Specify): spec.md exists & no clarifications needed
Phase 2 (Plan): plan.md exists & complete
@@ -56,40 +58,42 @@ Generate a dashboard view of all features and their completion status.
- Missing dependencies
5. **Generate Dashboard**:
```markdown
# Speckit Status Dashboard
**Generated**: [timestamp]
**Total Features**: X
## Overview
| Feature | Phase | Progress | Blockers | Next Action |
|---------|-------|----------|----------|-------------|
| auth-system | Implement | 75% | 0 | Complete remaining tasks |
| payment-flow | Plan | 40% | 2 | Resolve clarifications |
| Feature | Phase | Progress | Blockers | Next Action |
| ------------ | --------- | -------- | -------- | ------------------------ |
| auth-system | Implement | 75% | 0 | Complete remaining tasks |
| payment-flow | Plan | 40% | 2 | Resolve clarifications |
## Feature Details
### [Feature Name]
```
Spec: ████████░░ 80%
Plan: ██████████ 100%
Spec: ████████░░ 80%
Plan: ██████████ 100%
Tasks: ██████░░░░ 60%
```
**Blockers**:
- [ ] Clarification needed: "What payment providers?"
**Recent Activity**:
- Last modified: [date]
- Files changed: [list]
---
## Summary
- Features Ready for Implementation: X
- Features Blocked: Y
- Overall Project Completion: Z%
+2 -2
View File
@@ -4,7 +4,7 @@ description: Generate an actionable, dependency-ordered tasks.md for the feature
version: 1.0.0
depends-on:
- speckit-plan
handoffs:
handoffs:
- label: Analyze For Consistency
agent: speckit-analyze
prompt: Run a project analysis for consistency
@@ -96,7 +96,7 @@ Every task MUST strictly follow this format:
4. **[Story] label**: REQUIRED for user story phase tasks only
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
- Setup phase: NO story label
- Foundational phase: NO story label
- Foundational phase: NO story label
- User Story phases: MUST have story label
- Polish phase: NO story label
5. **Description**: Clear action with exact file path
@@ -1,6 +1,5 @@
---
description: "Task list template for feature implementation"
description: 'Task list template for feature implementation'
---
# Tasks: [FEATURE NAME]
@@ -25,21 +24,21 @@ description: "Task list template for feature implementation"
- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
- Paths shown below assume single project - adjust based on plan.md structure
<!--
<!--
============================================================================
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
The /speckit-tasks command MUST replace these with actual tasks based on:
- User stories from spec.md (with their priorities P1, P2, P3...)
- Feature requirements from plan.md
- Entities from data-model.md
- Endpoints from contracts/
Tasks MUST be organized by user story so each story can be:
- Implemented independently
- Tested independently
- Delivered as an MVP increment
DO NOT keep these sample tasks in the generated tasks.md file.
============================================================================
-->
@@ -83,8 +82,8 @@ Examples of foundational tasks (adjust based on your project):
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 1
@@ -107,8 +106,8 @@ Examples of foundational tasks (adjust based on your project):
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 2
@@ -129,8 +128,8 @@ Examples of foundational tasks (adjust based on your project):
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 3
+40 -37
View File
@@ -26,34 +26,35 @@ Detect the project's test framework, execute tests, and generate a comprehensive
### Execution Steps
1. **Detect Test Framework**:
```bash
# Check package.json for test frameworks
cat package.json 2>/dev/null | grep -E "(jest|vitest|mocha|ava|tap)"
# Check for Python test frameworks
ls pytest.ini setup.cfg pyproject.toml 2>/dev/null
# Check for Go tests
find . -name "*_test.go" -maxdepth 3 2>/dev/null | head -1
```
| Indicator | Framework |
|-----------|-----------|
| `jest` in package.json | Jest |
| `vitest` in package.json | Vitest |
| `pytest.ini` or `[tool.pytest]` | Pytest |
| `*_test.go` files | Go test |
| `Cargo.toml` + `#[test]` | Cargo test |
| Indicator | Framework |
| ------------------------------- | ---------- |
| `jest` in package.json | Jest |
| `vitest` in package.json | Vitest |
| `pytest.ini` or `[tool.pytest]` | Pytest |
| `*_test.go` files | Go test |
| `Cargo.toml` + `#[test]` | Cargo test |
2. **Run Tests with Coverage**:
| Framework | Command |
|-----------|---------|
| Jest | `npx jest --coverage --json --outputFile=coverage/test-results.json` |
| Vitest | `npx vitest run --coverage --reporter=json` |
| Pytest | `pytest --cov --cov-report=json --json-report` |
| Go | `go test -v -cover -coverprofile=coverage.out ./...` |
| Cargo | `cargo test -- --test-threads=1` |
| Framework | Command |
| --------- | -------------------------------------------------------------------- |
| Jest | `npx jest --coverage --json --outputFile=coverage/test-results.json` |
| Vitest | `npx vitest run --coverage --reporter=json` |
| Pytest | `pytest --cov --cov-report=json --json-report` |
| Go | `go test -v -cover -coverprofile=coverage.out ./...` |
| Cargo | `cargo test -- --test-threads=1` |
3. **Parse Test Results**:
Extract from test output:
@@ -70,39 +71,41 @@ Detect the project's test framework, execute tests, and generate a comprehensive
- Suggested fix (if pattern is recognizable)
5. **Generate Report**:
```markdown
# Test Report
**Date**: [timestamp]
**Framework**: [detected]
**Status**: PASS | FAIL
## Summary
| Metric | Value |
|--------|-------|
| Total Tests | X |
| Passed | X |
| Failed | X |
| Skipped | X |
| Duration | X.Xs |
| Coverage | X% |
| Metric | Value |
| ----------- | ----- |
| Total Tests | X |
| Passed | X |
| Failed | X |
| Skipped | X |
| Duration | X.Xs |
| Coverage | X% |
## Failed Tests
### [test name]
**File**: `path/to/test.ts:42`
**Error**: Expected X but received Y
**Suggestion**: Check mock setup for...
## Coverage by File
| File | Lines | Branches | Functions |
|------|-------|----------|-----------|
| src/auth.ts | 85% | 70% | 90% |
| File | Lines | Branches | Functions |
| ----------- | ----- | -------- | --------- |
| src/auth.ts | 85% | 70% | 90% |
## Next Actions
1. Fix failing test: [name]
2. Increase coverage in: [low coverage files]
```
+22 -21
View File
@@ -46,37 +46,38 @@ Post-implementation validation that compares code against spec requirements.
4. **Validation Checks**:
| Check | Method |
|-------|--------|
| Check | Method |
| -------------------- | ------------------------------------------------ |
| Requirement Coverage | Each requirement has ≥1 implementation reference |
| Acceptance Criteria | Each criterion is testable in code |
| Edge Case Handling | Each edge case has explicit handling code |
| Test Coverage | Each requirement has ≥1 test |
| Acceptance Criteria | Each criterion is testable in code |
| Edge Case Handling | Each edge case has explicit handling code |
| Test Coverage | Each requirement has ≥1 test |
5. **Generate Validation Report**:
```markdown
# Validation Report: [Feature Name]
**Date**: [timestamp]
**Status**: PASS | PARTIAL | FAIL
## Coverage Summary
| Metric | Count | Percentage |
|--------|-------|------------|
| Requirements Covered | X/Y | Z% |
| Acceptance Criteria Met | X/Y | Z% |
| Edge Cases Handled | X/Y | Z% |
| Tests Present | X/Y | Z% |
| Metric | Count | Percentage |
| ----------------------- | ----- | ---------- |
| Requirements Covered | X/Y | Z% |
| Acceptance Criteria Met | X/Y | Z% |
| Edge Cases Handled | X/Y | Z% |
| Tests Present | X/Y | Z% |
## Uncovered Requirements
| Requirement | Status | Notes |
|-------------|--------|-------|
| [REQ-001] | Missing | No implementation found |
| Requirement | Status | Notes |
| ----------- | ------- | ----------------------- |
| [REQ-001] | Missing | No implementation found |
## Recommendations
1. [Action item for gaps]
```