NestJS GraphQL API backend using CQRS, Prisma, and Bull queues. Use this skill when working with the API backend in apps/api, creating resolvers, services, commands, queries, or database operations.
This skill covers the NestJS GraphQL API backend that serves both web and native clients in the Game Critique project.
The API uses Command Query Responsibility Segregation:
// Command example
export class UpdateGameDataCommand {
constructor(public readonly hltbId: number) {}
}
// Command handler
@CommandHandler(UpdateGameDataCommand)
export class UpdateGameDataHandler {
async execute(command: UpdateGameDataCommand) {
// Implementation
}
}
// Query example
export class GetGamesQuery {
constructor(
public readonly search: string,
public readonly take: number,
public readonly skip: number
) {}
}
Each feature follows this structure:
modules/
<feature>/
commands/ # CQRS commands
<command_name>/
<command_name>.command.ts
<command_name>.handler.ts
queries/ # CQRS queries
<query_name>/
<query_name>.query.ts
<query_name>.handler.ts
<feature>.module.ts # NestJS module
<feature>.service.ts # Business logic
<feature>.resolver.ts # GraphQL resolver
<feature>.repository.ts # Data access layer
<feature>.dto.ts # DTOs and GraphQL types
<feature>.consumer.ts # Bull queue consumer (if needed)
Data access is abstracted through repositories:
@Injectable()
export class GamesRepository {
constructor(private readonly prisma: PrismaService) {}
async getGameById(hltbId: number) {
return this.prisma.game.findUnique({
where: { hltbId },
include: {
cover: true,
platformForGame: { include: { platform: true } },
genres: { include: { genre: true } },
release: true,
completionTime: true,
},
});
}
}
Schema is generated from TypeScript decorators:
// DTOs with GraphQL decorators
@ObjectType()
export class GameWithAllDataDTO {
@Field(() => Int)
hltbId: number;
@Field()
title: string;
@Field(() => [PlatformDTO])
platforms: PlatformDTO[];
}
@ArgsType()
export class GetPaginatedGamesArgs {
@Field({ nullable: true })
search?: string;
@Field(() => Int, { defaultValue: 10 })
take: number;
@Field(() => Int, { defaultValue: 0 })
skip: number;
}
// Resolver
@Resolver()
export class GamesResolver {
constructor(private readonly gamesService: GamesService) {}
@Query(() => GameWithAllDataDTO, { name: 'game' })
async getGameById(@Args('hltbId') hltbId: number) {
return this.gamesService.getGameById(hltbId);
}
@UseGuards(JwtAuthGuard, AdminUserGuard)
@Mutation(() => UpdateGameDataDTO, { name: 'updateGameData' })
async updateGameData(@Args('hltbId') hltbId: number) {
return this.gamesService.updateGameData(hltbId);
}
}
// JWT Authentication
@UseGuards(JwtAuthGuard)
@Query(() => UserProfile)
async getProfile(@CurrentUser() user: User) {
return this.profilesService.getProfile(user.oauthId);
}
// Admin-only operations
@UseGuards(JwtAuthGuard, AdminUserGuard)
@Mutation(() => Boolean)
async deleteUser(@Args('userId') userId: number) {
return this.usersService.deleteUser(userId);
}
model User {
id Int @id @default(autoincrement())
oauthId String @unique @map("oauth_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
profile Profile?
GamesStatus GamesStatus[]
role UserRole?
@@map("users")
}
# 1. Update schema.prisma
# 2. Create migration
yarn prisma-migrate-dev <migration-name>
# 3. Generate client
yarn prisma-generate
# 4. Update repositories and DTOs
// In module
BullModule.registerQueue({
name: 'games',
}),
// In service
constructor(
@InjectQueue('games') private gamesQueue: Queue,
) {}
async addGamesToDatabase(games: GameData[]) {
return this.gamesQueue.add('createGame', games);
}
// Consumer
@Processor('games')
export class GamesConsumer {
@Process('createGame')
async handleCreateGame(job: Job<GameData[]>) {
// Process job
}
}
Always use proper HTTP exceptions:
import { HttpException, HttpStatus } from '@nestjs/common';
if (!game) {
throw new HttpException(
{
status: HttpStatus.NOT_FOUND,
message: 'Nie znaleziono gry o podanym ID',
},
HttpStatus.NOT_FOUND,
);
}
# Development
yarn dev # Watch mode
yarn start:debug # Debug mode
# Building
yarn build # Production build with SWC
# Database
yarn start:db # Start PostgreSQL + Redis with Docker
yarn prisma-generate # Generate Prisma client
yarn prisma-migrate-dev # Create new migration
yarn prisma-seed # Seed database
# Testing
yarn test:implement # Run unit tests
yarn test:e2e # Run e2e tests
yarn test:cov # Coverage report
# Code Quality
yarn lint # ESLint
yarn format # Prettier
# 1. Generate module
nest g module modules/<feature>
# 2. Generate service and resolver
nest g service modules/<feature>
nest g resolver modules/<feature>
# 3. Create repository
# Create <feature>.repository.ts
# 4. Create DTOs with GraphQL decorators
# Create <feature>.dto.ts
# 5. Implement CQRS if needed
mkdir -p src/modules/<feature>/commands
mkdir -p src/modules/<feature>/queries
# 6. Add guards for protected operations
# 7. Register in app.module.ts
# 8. Schema auto-generates on startup
describe('GamesService', () => {
let service: GamesService;
let repository: GamesRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
GamesService,
{
provide: GamesRepository,
useValue: {
getGameById: jest.fn(),
},
},
],
}).compile();
service = module.get<GamesService>(GamesService);
repository = module.get<GamesRepository>(GamesRepository);
});
it('should return a game by id', async () => {
const mockGame = { hltbId: 1, title: 'Test Game' };
jest.spyOn(repository, 'getGameById').mockResolvedValue(mockGame);
const result = await service.getGameById(1);
expect(result).toEqual(mockGame);
});
});
Required environment variables:
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/game_critique
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# Auth0
AUTH0_DOMAIN=your-domain.auth0.com
AUTH0_AUDIENCE=your-audience
# External APIs
IGDB_CLIENT_ID=your-client-id
IGDB_CLIENT_SECRET=your-client-secret
# Cloudinary
CLOUDINARY_CLOUD_NAME=your-cloud
CLOUDINARY_API_KEY=your-key
CLOUDINARY_API_SECRET=your-secret
# Sentry
SENTRY_DSN=your-dsn
# App
NODE_ENV=development
PORT=3000
@Injectable()
export class GamesService {
constructor(
private readonly queryBus: QueryBus,
private readonly commandBus: CommandBus,
) {}
async getPaginatedGames(args: GetPaginatedGamesArgs) {
return this.queryBus.execute(
new GetGamesQuery(args.search, args.take, args.skip)
);
}
async updateGameData(hltbId: number) {
return this.commandBus.execute(
new UpdateGameDataCommand(hltbId)
);
}
}
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async handleDailyTask() {
// Execute task
}
}
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer