
Nodejs
- 63 installs
- 101 repo stars
- Updated November 28, 2025
- blencorp/claude-code-kit
Node.js is a Claude Code skill that provides core TypeScript Node.js backend patterns for error handling, configuration, testing, and layered architecture.
About
Node.js is a skill that gives Claude core Node.js backend patterns for TypeScript, covering async/await error handling, configuration management, testing strategies, and layered architecture. A developer uses it when building Node.js backend services, APIs, or microservices. It follows a routes to controllers to services to repositories flow.
- Layered architecture: routes to controllers to services to repositories
- Async/await error handling patterns for controllers and parallel operations
- Config management with zod-validated environment variables
Nodejs by the numbers
- 63 all-time installs (skills.sh)
- Ranked #3,135 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
nodejs capabilities & compatibility
- Capabilities
- api development · backend patterns · testing
- Use cases
- api development · testing
- IDEs
- vscode · cursor ide
What nodejs says it does
Core Node.js backend patterns for TypeScript applications including async/await error handling, middleware concepts, configuration management, testing strategies, and layered architecture principles.
Core patterns for building scalable Node.js backend applications with TypeScript, emphasizing clean architecture, error handling, and testability.
npx skills add https://github.com/blencorp/claude-code-kit --skill nodejsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 63 |
|---|---|
| repo stars | ★ 101 |
| Last updated | November 28, 2025 |
| Repository | blencorp/claude-code-kit ↗ |
What it does
Build TypeScript Node.js backend services with layered architecture and error handling.
Who is it for?
Structuring TypeScript Node.js backend services with clean architecture
Skip if: Frontend code or non-Node backends
When should I use this skill?
Building Node.js backend services, APIs, or microservices
What you get
Services follow a layered architecture with typed error handling and validated config.
- Controllers
- Services
- Repositories
By the numbers
- 4-layer architecture: routes, controllers, services, repositories
Files
Node.js Backend Patterns
Purpose
Core patterns for building scalable Node.js backend applications with TypeScript, emphasizing clean architecture, error handling, and testability.
When to Use This Skill
- Building Node.js backend services
- Implementing async/await patterns
- Error handling and logging
- Configuration management
- Testing backend code
- Layered architecture (routes → controllers → services → repositories)
---
Quick Start
Layered Architecture
src/
├── api/
│ ├── routes/ # HTTP route definitions
│ ├── controllers/ # Request/response handling
│ ├── services/ # Business logic
│ └── repositories/ # Data access
├── middleware/ # Express middleware
├── types/ # TypeScript types
├── config/ # Configuration
└── utils/ # UtilitiesFlow: Route → Controller → Service → Repository → Database
---
Async/Await Error Handling
Basic Pattern
async function fetchUser(id: string): Promise<User> {
try {
const user = await db.user.findUnique({ where: { id } });
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
console.error('Error fetching user:', error);
throw error;
}
}Async Controller Pattern
class UserController {
async getUser(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const user = await this.userService.getById(id);
res.json({
success: true,
data: user,
});
} catch (error) {
console.error('Error in getUser:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch user',
});
}
}
}Promise.all for Parallel Operations
async function getUserDashboard(userId: string) {
try {
const [user, posts, followers] = await Promise.all([
userService.getById(userId),
postService.getByUser(userId),
followerService.getByUser(userId),
]);
return { user, posts, followers };
} catch (error) {
console.error('Error loading dashboard:', error);
throw error;
}
}---
TypeScript Patterns
Request/Response Types
// Request body
interface CreateUserRequest {
email: string;
name: string;
password: string;
}
// Response
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// Usage
async function createUser(
req: Request<{}, {}, CreateUserRequest>,
res: Response<ApiResponse<User>>
): Promise<void> {
const { email, name, password } = req.body;
const user = await userService.create({ email, name, password });
res.json({
success: true,
data: user,
});
}Service Layer Types
interface IUserService {
getById(id: string): Promise<User>;
create(data: CreateUserDto): Promise<User>;
update(id: string, data: UpdateUserDto): Promise<User>;
delete(id: string): Promise<void>;
}
class UserService implements IUserService {
async getById(id: string): Promise<User> {
// Implementation
}
async create(data: CreateUserDto): Promise<User> {
// Implementation
}
async update(id: string, data: UpdateUserDto): Promise<User> {
// Implementation
}
async delete(id: string): Promise<void> {
// Implementation
}
}---
Configuration Management
Environment Variables
// config/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
});
export const env = envSchema.parse(process.env);Unified Config
// config/index.ts
interface Config {
server: {
port: number;
host: string;
};
database: {
url: string;
};
auth: {
jwtSecret: string;
jwtExpiry: string;
};
}
export const config: Config = {
server: {
port: parseInt(process.env.PORT || '3000'),
host: process.env.HOST || 'localhost',
},
database: {
url: process.env.DATABASE_URL || '',
},
auth: {
jwtSecret: process.env.JWT_SECRET || '',
jwtExpiry: process.env.JWT_EXPIRY || '7d',
},
};---
Layered Architecture
Controller Layer
// controllers/UserController.ts
export class UserController {
constructor(private userService: UserService) {}
async getById(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const user = await this.userService.getById(id);
res.json({
success: true,
data: user,
});
}
async create(req: Request, res: Response): Promise<void> {
const userData = req.body;
const user = await this.userService.create(userData);
res.status(201).json({
success: true,
data: user,
});
}
}Service Layer
// services/UserService.ts
export class UserService {
constructor(private userRepository: UserRepository) {}
async getById(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
return user;
}
async create(data: CreateUserDto): Promise<User> {
// Business logic
const hashedPassword = await this.hashPassword(data.password);
return this.userRepository.create({
...data,
password: hashedPassword,
});
}
private async hashPassword(password: string): Promise<string> {
// Hash implementation
return password; // Placeholder
}
}Repository Layer
// repositories/UserRepository.ts
export class UserRepository {
async findById(id: string): Promise<User | null> {
// Database query
return db.user.findUnique({ where: { id } });
}
async create(data: CreateUserData): Promise<User> {
return db.user.create({ data });
}
async update(id: string, data: UpdateUserData): Promise<User> {
return db.user.update({
where: { id },
data,
});
}
async delete(id: string): Promise<void> {
await db.user.delete({ where: { id } });
}
}---
Dependency Injection
Basic DI Pattern
// Composition root
const userRepository = new UserRepository();
const userService = new UserService(userRepository);
const userController = new UserController(userService);
export { userController };Service Container
// container.ts
class Container {
private services: Map<string, any> = new Map();
register<T>(name: string, factory: () => T): void {
this.services.set(name, factory());
}
get<T>(name: string): T {
const service = this.services.get(name);
if (!service) {
throw new Error(`Service ${name} not found`);
}
return service;
}
}
export const container = new Container();
// Register services
container.register('userRepository', () => new UserRepository());
container.register('userService', () => new UserService(
container.get('userRepository')
));
container.register('userController', () => new UserController(
container.get('userService')
));---
Error Handling
Custom Error Classes
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404);
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 400);
}
}
// Usage
async function getUser(id: string): Promise<User> {
const user = await userRepository.findById(id);
if (!user) {
throw new NotFoundError('User');
}
return user;
}Async Error Wrapper
type AsyncHandler = (
req: Request,
res: Response,
next: NextFunction
) => Promise<void>;
export const asyncHandler = (fn: AsyncHandler) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Usage
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await userService.getById(req.params.id);
res.json({ data: user });
}));---
---
Best Practices
1. Always Use Async/Await
// ✅ Good: async/await
async function getUser(id: string): Promise<User> {
const user = await userRepository.findById(id);
return user;
}
// ❌ Avoid: Promise chains
function getUser(id: string): Promise<User> {
return userRepository.findById(id)
.then(user => user)
.catch(error => throw error);
}2. Layer Separation
// ✅ Good: Separated layers
// Controller handles HTTP
// Service handles business logic
// Repository handles data access
// ❌ Avoid: Business logic in controllers
class UserController {
async create(req: Request, res: Response) {
// ❌ Don't put business logic here
const hashedPassword = await hash(req.body.password);
const user = await db.user.create({...});
res.json(user);
}
}3. Type Everything
// ✅ Good: Full type coverage
async function updateUser(
id: string,
data: UpdateUserDto
): Promise<User> {
return userService.update(id, data);
}
// ❌ Avoid: any types
async function updateUser(id: any, data: any): Promise<any> {
return userService.update(id, data);
}---
Additional Resources
For more patterns, see:
- async-and-errors.md - Advanced error handling
- testing-guide.md - Comprehensive testing
- architecture-patterns.md - Architecture details
Architecture Patterns for Node.js/TypeScript
Layered Architecture Overview
┌─────────────────────────────────────────┐
│ Routes (HTTP Layer) │
│ - Define endpoints │
│ - Route parameters │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Controllers (API Layer) │
│ - Request/Response handling │
│ - Input validation │
│ - Response formatting │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Services (Business Logic) │
│ - Business rules │
│ - Orchestration │
│ - Transaction management │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Repositories (Data Access) │
│ - Database queries │
│ - ORM interactions │
│ - Data mapping │
└──────────────────────────────────────────┘Route Layer
Defining Routes
// src/api/routes/user.routes.ts
import { Router } from 'express';
import { UserController } from '../controllers/user.controller';
const router = Router();
const userController = new UserController();
router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.post('/', userController.create);
router.patch('/:id', userController.update);
router.delete('/:id', userController.delete);
export default router;Route Registration
// src/app.ts
import express from 'express';
import userRoutes from './api/routes/user.routes';
import postRoutes from './api/routes/post.routes';
const app = express();
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);
export default app;Controller Layer
Controller Pattern
// src/api/controllers/user.controller.ts
import { Request, Response } from 'express';
import { UserService } from '../services/user.service';
import { CreateUserDto, UpdateUserDto } from '../types/user.types';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
getAll = async (req: Request, res: Response) => {
try {
const users = await this.userService.getAll();
res.json({ success: true, data: users });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
};
getById = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const user = await this.userService.getById(id);
res.json({ success: true, data: user });
} catch (error) {
res.status(404).json({ success: false, error: error.message });
}
};
create = async (req: Request, res: Response) => {
try {
const userData: CreateUserDto = req.body;
const user = await this.userService.create(userData);
res.status(201).json({ success: true, data: user });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
};
}Service Layer
Service Pattern
// src/api/services/user.service.ts
import { UserRepository } from '../repositories/user.repository';
import { CreateUserDto, UpdateUserDto, User } from '../types/user.types';
import { NotFoundError, ValidationError } from '../errors';
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
async getAll(): Promise<User[]> {
return await this.userRepository.findAll();
}
async getById(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundError(`User with id ${id} not found`);
}
return user;
}
async create(data: CreateUserDto): Promise<User> {
// Business logic: Validate email
if (!this.isValidEmail(data.email)) {
throw new ValidationError('Invalid email format');
}
// Business logic: Check for duplicate
const existing = await this.userRepository.findByEmail(data.email);
if (existing) {
throw new ValidationError('Email already exists');
}
// Business logic: Hash password
const hashedPassword = await this.hashPassword(data.password);
return await this.userRepository.create({
...data,
password: hashedPassword
});
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
private async hashPassword(password: string): Promise<string> {
// Implementation
return password; // placeholder
}
}Repository Layer
Repository Pattern
// src/api/repositories/user.repository.ts
import { prisma } from '../../database';
import { User, CreateUserDto, UpdateUserDto } from '../types/user.types';
export class UserRepository {
async findAll(): Promise<User[]> {
return await prisma.user.findMany();
}
async findById(id: string): Promise<User | null> {
return await prisma.user.findUnique({
where: { id }
});
}
async findByEmail(email: string): Promise<User | null> {
return await prisma.user.findUnique({
where: { email }
});
}
async create(data: CreateUserDto): Promise<User> {
return await prisma.user.create({
data
});
}
async update(id: string, data: UpdateUserDto): Promise<User> {
return await prisma.user.update({
where: { id },
data
});
}
async delete(id: string): Promise<void> {
await prisma.user.delete({
where: { id }
});
}
}Dependency Injection
Constructor Injection
export class UserService {
constructor(private userRepository: UserRepository) {}
async getById(id: string): Promise<User> {
return await this.userRepository.findById(id);
}
}
// Usage
const userRepository = new UserRepository();
const userService = new UserService(userRepository);Service Container
// src/container.ts
export class ServiceContainer {
private services = new Map<string, any>();
register<T>(name: string, factory: () => T): void {
this.services.set(name, factory);
}
resolve<T>(name: string): T {
const factory = this.services.get(name);
if (!factory) {
throw new Error(`Service ${name} not found`);
}
return factory();
}
}
// Setup
const container = new ServiceContainer();
container.register('UserRepository', () => new UserRepository());
container.register('UserService', () =>
new UserService(container.resolve('UserRepository'))
);
// Usage
const userService = container.resolve<UserService>('UserService');Transaction Management
Using Transactions
export class OrderService {
async createOrder(orderData: CreateOrderDto): Promise<Order> {
return await prisma.$transaction(async (tx) => {
// Create order
const order = await tx.order.create({
data: orderData
});
// Reduce inventory
await tx.inventory.update({
where: { productId: orderData.productId },
data: { quantity: { decrement: orderData.quantity } }
});
// Create payment record
await tx.payment.create({
data: {
orderId: order.id,
amount: order.totalAmount
}
});
return order;
});
}
}Middleware Pattern
Custom Middleware
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '../utils/jwt';
export function authMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
// Usage
router.get('/profile', authMiddleware, userController.getProfile);Best Practices
1. Single Responsibility
Each layer has one job:
- Routes: Define endpoints
- Controllers: Handle HTTP
- Services: Business logic
- Repositories: Data access
2. Dependency Direction
Dependencies flow downward:
Routes → Controllers → Services → Repositories3. Keep Controllers Thin
// ✅ Good: Thin controller
async getUser(req: Request, res: Response) {
const user = await this.userService.getById(req.params.id);
res.json({ data: user });
}
// ❌ Bad: Fat controller with business logic
async getUser(req: Request, res: Response) {
const user = await prisma.user.findUnique({ where: { id: req.params.id } });
if (!user) throw new Error('Not found');
if (user.status === 'banned') throw new Error('User banned');
// ... more logic
res.json({ data: user });
}4. Keep Services Focused
// ✅ Good: Focused service
class UserService {
async create(data: CreateUserDto): Promise<User> {
// User-specific business logic only
}
}
// ❌ Bad: Service doing too much
class UserService {
async create(data: CreateUserDto): Promise<User> {
// Creates user
// Sends email
// Updates analytics
// Notifies admins
// ...
}
}5. Type Everything
Use TypeScript types for all DTOs, entities, and service responses.
export interface CreateUserDto {
email: string;
name: string;
password: string;
}
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}Advanced Async Patterns and Error Handling
Async/Await Best Practices
Error Handling Patterns
// Pattern 1: Try-Catch in async functions
async function fetchUserData(userId: string): Promise<User> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user data');
}
}
// Pattern 2: Async wrapper for error handling
function asyncHandler(fn: Function) {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Usage
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await userService.getById(req.params.id);
res.json({ success: true, data: user });
}));Custom Error Classes
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(message: string = 'Resource not found') {
super(message, 404);
}
}
export class ValidationError extends AppError {
constructor(message: string = 'Validation failed') {
super(message, 400);
}
}Promise.all for Parallel Operations
// Execute multiple async operations in parallel
async function getUserDashboard(userId: string) {
const [user, posts, comments, followers] = await Promise.all([
userService.getById(userId),
postService.getByUserId(userId),
commentService.getByUserId(userId),
followerService.getFollowers(userId)
]);
return { user, posts, comments, followers };
}Promise.allSettled for Handling Partial Failures
async function fetchMultipleResources(ids: string[]) {
const results = await Promise.allSettled(
ids.map(id => fetchResource(id))
);
const successful = results
.filter((result): result is PromiseFulfilledResult<Resource> =>
result.status === 'fulfilled'
)
.map(result => result.value);
const failed = results
.filter((result): result is PromiseRejectedResult =>
result.status === 'rejected'
)
.map(result => result.reason);
return { successful, failed };
}Retry Logic
async function fetchWithRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError!.message}`);
}
// Usage
const data = await fetchWithRetry(() => fetch('/api/data'));Timeout Handling
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Operation timed out')), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
const data = await withTimeout(fetchData(), 5000); // 5 second timeoutError Boundary Middleware
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: err.message
});
}
// Log unexpected errors
console.error('Unexpected error:', err);
res.status(500).json({
success: false,
error: 'Internal server error'
});
}Async Patterns
Sequential vs Parallel Execution
// ❌ Sequential (slow)
const user = await getUser(id);
const posts = await getPosts(id);
const comments = await getComments(id);
// ✅ Parallel (fast)
const [user, posts, comments] = await Promise.all([
getUser(id),
getPosts(id),
getComments(id)
]);Dependent Async Operations
async function processOrder(orderId: string) {
// Step 1: Get order
const order = await orderService.getById(orderId);
// Step 2: Process payment (depends on order)
const payment = await paymentService.process(order.totalAmount);
// Step 3: Update inventory (depends on order)
await inventoryService.reduce(order.items);
// Step 4: Send confirmation (depends on payment)
await emailService.sendConfirmation(order.email, payment.receiptId);
return { order, payment };
}Best Practices
1. Always handle errors - Never let promises reject silently 2. Use async/await - More readable than promise chains 3. Parallelize when possible - Use Promise.all for independent operations 4. Add timeouts - Prevent hanging requests 5. Use custom error classes - Make error handling type-safe 6. Log errors properly - Include context and stack traces 7. Test error paths - Don't just test happy paths
Comprehensive Testing Guide for Node.js/TypeScript
Unit Testing Services
Basic Service Test
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { UserService } from './user.service';
import type { UserRepository } from './user.repository';
describe('UserService', () => {
let userService: UserService;
let mockRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepository = {
findById: vi.fn(),
findByEmail: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
} as jest.Mocked<UserRepository>;
userService = new UserService(mockRepository);
});
describe('getById', () => {
it('should return user when found', async () => {
const mockUser = {
id: '123',
email: 'test@example.com',
name: 'Test User'
};
mockRepository.findById.mockResolvedValue(mockUser);
const result = await userService.getById('123');
expect(result).toEqual(mockUser);
expect(mockRepository.findById).toHaveBeenCalledWith('123');
expect(mockRepository.findById).toHaveBeenCalledTimes(1);
});
it('should throw NotFoundError when user not found', async () => {
mockRepository.findById.mockResolvedValue(null);
await expect(userService.getById('123')).rejects.toThrow('User not found');
});
});
describe('create', () => {
it('should create user with hashed password', async () => {
const userData = {
email: 'new@example.com',
name: 'New User',
password: 'password123'
};
const createdUser = {
id: '456',
...userData,
password: 'hashed_password'
};
mockRepository.create.mockResolvedValue(createdUser);
const result = await userService.create(userData);
expect(result).toEqual(createdUser);
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
email: userData.email,
name: userData.name
})
);
});
it('should throw ValidationError for invalid email', async () => {
const userData = {
email: 'invalid-email',
name: 'Test',
password: 'password123'
};
await expect(userService.create(userData)).rejects.toThrow('Invalid email');
});
});
});Integration Testing
API Endpoint Testing
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import request from 'supertest';
import { app } from '../app';
import { prisma } from '../database';
describe('User API Integration Tests', () => {
beforeAll(async () => {
// Setup test database
await prisma.$connect();
});
afterAll(async () => {
// Cleanup
await prisma.$disconnect();
});
beforeEach(async () => {
// Clear database between tests
await prisma.user.deleteMany();
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'securePassword123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('id');
expect(response.body.data.email).toBe(userData.email);
expect(response.body.data).not.toHaveProperty('password'); // Password should not be returned
});
it('should return 400 for duplicate email', async () => {
const userData = {
email: 'duplicate@example.com',
name: 'User 1',
password: 'password123'
};
// Create first user
await request(app).post('/api/users').send(userData);
// Try to create duplicate
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('already exists');
});
it('should return 400 for missing required fields', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com' }) // Missing name and password
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBeDefined();
});
});
describe('GET /api/users/:id', () => {
it('should return user by id', async () => {
// Create user first
const createResponse = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
name: 'Test User',
password: 'password123'
});
const userId = createResponse.body.data.id;
// Get user
const response = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.id).toBe(userId);
expect(response.body.data.email).toBe('test@example.com');
});
it('should return 404 for non-existent user', async () => {
const response = await request(app)
.get('/api/users/non-existent-id')
.expect(404);
expect(response.body.success).toBe(false);
});
});
describe('PATCH /api/users/:id', () => {
it('should update user', async () => {
// Create user
const createResponse = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
name: 'Original Name',
password: 'password123'
});
const userId = createResponse.body.data.id;
// Update user
const response = await request(app)
.patch(`/api/users/${userId}`)
.send({ name: 'Updated Name' })
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe('Updated Name');
expect(response.body.data.email).toBe('test@example.com'); // Unchanged
});
});
});Mocking External Dependencies
Mocking Prisma
import { vi } from 'vitest';
import { PrismaClient } from '@prisma/client';
// Create mock Prisma client
const mockPrisma = {
user: {
findUnique: vi.fn(),
findMany: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
$transaction: vi.fn(),
} as unknown as PrismaClient;
// Use in tests
mockPrisma.user.findUnique.mockResolvedValue({
id: '1',
email: 'test@example.com',
name: 'Test User'
});Mocking HTTP Requests
import { vi } from 'vitest';
import axios from 'axios';
vi.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
// In test
mockedAxios.get.mockResolvedValue({
data: { message: 'Success' },
status: 200
});Testing Async Error Handling
describe('Error Handling', () => {
it('should handle service errors gracefully', async () => {
mockRepository.findById.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/api/users/123')
.expect(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Internal server error');
});
it('should handle validation errors', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'invalid', name: '', password: '123' })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.errors).toBeDefined();
expect(response.body.errors).toHaveLength(3);
});
});Test Utilities
Test Data Factory
// test/factories/user.factory.ts
import { faker } from '@faker-js/faker';
export const createUserData = (overrides?: Partial<User>) => ({
email: faker.internet.email(),
name: faker.person.fullName(),
password: faker.internet.password(),
...overrides
});
// Usage in tests
const userData = createUserData({ email: 'specific@example.com' });Test Database Setup
// test/setup.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.TEST_DATABASE_URL
}
}
});
export async function setupTestDatabase() {
await prisma.$executeRawUnsafe('DROP SCHEMA IF EXISTS test CASCADE');
await prisma.$executeRawUnsafe('CREATE SCHEMA test');
// Run migrations
}
export async function teardownTestDatabase() {
await prisma.$disconnect();
}Testing Best Practices
1. Test Isolation
beforeEach(async () => {
// Clear all data
await prisma.user.deleteMany();
await prisma.post.deleteMany();
});2. Test Naming
// ✅ Good: Descriptive test names
it('should return 404 when user does not exist', async () => {});
// ❌ Bad: Vague test names
it('handles errors', async () => {});3. AAA Pattern
it('should create user', async () => {
// Arrange
const userData = { email: 'test@example.com', name: 'Test' };
// Act
const result = await userService.create(userData);
// Assert
expect(result.email).toBe(userData.email);
});4. Mock Only What You Need
// ✅ Good: Mock only external dependencies
const mockRepository = { findById: vi.fn() };
// ❌ Bad: Mocking everything including business logic
const mockService = { create: vi.fn(), update: vi.fn(), delete: vi.fn() };Coverage Goals
- Unit Tests: Aim for 80%+ coverage of business logic
- Integration Tests: Cover all API endpoints
- E2E Tests: Cover critical user flows
Run coverage:
npm run test:coverage{
"nodejs": {
"type": "domain",
"enforcement": "suggest",
"priority": "medium",
"promptTriggers": {
"keywords": [
"node.js",
"nodejs",
"process.env",
"process.argv",
"process.cwd",
"require(",
"module.exports",
"__dirname",
"__filename",
"fs.readFile",
"fs.writeFile",
"fs.promises",
"path.join",
"path.resolve",
"http.createServer",
"https.createServer",
"EventEmitter",
"Buffer",
"stream.Readable",
"stream.Writable",
"child_process",
"os.platform",
"crypto.createHash",
"util.promisify"
],
"intentPatterns": [
"create.*node.*server",
"read.*file.*node",
"write.*file.*node",
"use.*node.*fs",
"create.*node.*stream",
"spawn.*child.*process",
"use.*buffer",
"create.*http.*server",
"use.*path.*module",
"handle.*process.*events",
"use.*node.*(fs|path|http|crypto|stream)"
]
},
"fileTriggers": {
"pathPatterns": [
"**/services/**/*.ts",
"**/services/**/*.js",
"**/utils/**/*.ts",
"**/utils/**/*.js",
"**/lib/**/*.ts",
"**/lib/**/*.js",
"**/scripts/**/*.js",
"**/scripts/**/*.ts"
],
"contentPatterns": [
"import.*fs.*from 'fs'",
"import.*path.*from 'path'",
"import.*http.*from 'http'",
"require\\('fs'\\)",
"require\\('path'\\)",
"require\\('http'\\)",
"process\\.env",
"__dirname",
"__filename",
"Buffer\\.",
"createServer\\(",
"EventEmitter"
]
}
}
}
Related skills
FAQ
What architecture does this skill recommend?
A layered architecture with routes, controllers, services, and repositories.
How are environment variables handled?
They are validated with a zod schema in a config module before use.