Node.js/TypeScript backend expert. Handles Express/Fastify API routes, TypeScript strict mode, Prisma ORM, Zod validation, error handling, configuration management. Use when project is Node.js backend (package.json + TypeScript server).
Scanned 9/8/2026
Install to Claude Code
npx -y skills add mattnigh/skills_collection --skill collection --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Collection?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mattnigh-collection-b0db963f)More formats (shields.io, HTML) on the badges page.
---
name: backend-nodejs
description: Node.js/TypeScript backend expert. Handles Express/Fastify API routes, TypeScript strict mode, Prisma ORM, Zod validation, error handling, configuration management. Use when project is Node.js backend (package.json + TypeScript server).
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
---
# Backend Node.js Skill - API Development Expert
> **Expert backend Node.js/TypeScript pour API REST + GraphQL**
>
> Inspiré de : Vercel API design, Stripe backend patterns, Prisma best practices
---
## Scope
**Chargé par:** executor agent (SI projet Node.js détecté)
**Détection auto:**
- Fichier `package.json` existe
- Fichiers `server.ts` ou `index.ts` ou `src/server.ts`
- Dependencies: `express`, `fastify`, `@prisma/client`, etc
**Stack supporté:**
- **Runtime:** Node.js 18+ / Bun
- **Language:** TypeScript (strict mode)
- **Frameworks:** Express, Fastify, Hono
- **Database:** Prisma ORM
- **Validation:** Zod
- **Testing:** Vitest, Jest
---
## Conventions Strictes
### 1. Structure Projet (Obligatoire)
```
backend/
├── src/
│ ├── config/
│ │ └── env.ts # Configuration centralisée (1 seul fichier)
│ ├── routes/
│ │ ├── index.ts # Router principal
│ │ ├── tasks.ts
│ │ └── users.ts
│ ├── services/
│ │ ├── task.service.ts # Business logic
│ │ └── user.service.ts
│ ├── middleware/
│ │ ├── auth.ts
│ │ ├── error.ts
│ │ └── validation.ts
│ ├── types/
│ │ └── index.ts # TypeScript types partagés
│ ├── lib/
│ │ ├── prisma.ts # Prisma client singleton
│ │ └── logger.ts
│ └── server.ts # Entry point
├── prisma/
│ └── schema.prisma
├── package.json
├── tsconfig.json
└── .env
```
---
### 2. Configuration (1 seul fichier - 12-Factor App)
```typescript
// src/config/env.ts
import { z } from 'zod'
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().default('3000'),
DATABASE_URL: z.string(),
JWT_SECRET: z.string().min(32),
CORS_ORIGIN: z.string().default('http://localhost:3000'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
})
export const env = envSchema.parse(process.env)
export type Env = z.infer<typeof envSchema>
```
**Principe:** Validation config au démarrage, crash immédiat si config invalide.
---
### 3. Prisma Client Singleton (Pattern Vercel)
```typescript
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { env } from '@/config/env'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
})
if (env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
```
**Interdiction:** Créer plusieurs instances PrismaClient (épuise connexions DB).
---
### 4. Routes Pattern (Express)
```typescript
// src/routes/tasks.ts
import { Router } from 'express'
import { z } from 'zod'
import { taskService } from '@/services/task.service'
import { validateRequest } from '@/middleware/validation'
import { requireAuth } from '@/middleware/auth'
const router = Router()
// Zod schemas
const createTaskSchema = z.object({
body: z.object({
title: z.string().min(1).max(200),
description: z.string().optional(),
status: z.enum(['PENDING', 'IN_PROGRESS', 'COMPLETED']).default('PENDING'),
}),
})
// Routes
router.post(
'/',
requireAuth,
validateRequest(createTaskSchema),
async (req, res, next) => {
try {
const task = await taskService.createTask(req.user.id, req.body)
res.status(201).json(task)
} catch (error) {
next(error)
}
}
)
router.get(
'/',
requireAuth,
async (req, res, next) => {
try {
const tasks = await taskService.getTasks(req.user.id)
res.json(tasks)
} catch (error) {
next(error)
}
}
)
router.patch(
'/:id',
requireAuth,
validateRequest(z.object({
params: z.object({ id: z.string().cuid() }),
body: createTaskSchema.shape.body.partial(),
})),
async (req, res, next) => {
try {
const task = await taskService.updateTask(
req.params.id,
req.user.id,
req.body
)
res.json(task)
} catch (error) {
next(error)
}
}
)
export default router
```
**Pattern:** Routes légères → Business logic dans services.
---
### 5. Services Pattern (Singleton + Business Logic)
```typescript
// src/services/task.service.ts
import { prisma } from '@/lib/prisma'
import { TaskStatus } from '@prisma/client'
export class TaskService {
async createTask(
userId: string,
data: { title: string; description?: string; status?: TaskStatus }
) {
// Business logic validation
const existingCount = await prisma.task.count({
where: { userId, status: 'PENDING' },
})
if (existingCount >= 100) {
throw new Error('Maximum pending tasks limit reached')
}
// Create with transaction if multiple operations
return prisma.task.create({
data: {
...data,
userId,
},
include: {
user: {
select: { id: true, name: true, email: true },
},
},
})
}
async getTasks(userId: string) {
return prisma.task.findMany({
where: { userId },
include: {
user: {
select: { id: true, name: true },
},
},
orderBy: { createdAt: 'desc' },
})
}
async updateTask(
id: string,
userId: string,
data: Partial<{ title: string; description?: string; status?: TaskStatus }>
) {
// Check ownership
const task = await prisma.task.findUnique({
where: { id },
select: { userId: true },
})
if (!task || task.userId !== userId) {
throw new Error('Task not found or access denied')
}
return prisma.task.update({
where: { id },
data,
})
}
async deleteTask(id: string, userId: string) {
// Check ownership
const task = await prisma.task.findUnique({
where: { id },
select: { userId: true },
})
if (!task || task.userId !== userId) {
throw new Error('Task not found or access denied')
}
return prisma.task.delete({
where: { id },
})
}
}
// Singleton export
export const taskService = new TaskService()
```
**Principe:** 1 service = 1 ressource, toute business logic centralisée.
---
### 6. Error Handling Middleware (Standardisé)
```typescript
// src/middleware/error.ts
import { Request, Response, NextFunction } from 'express'
import { ZodError } from 'zod'
import { Prisma } from '@prisma/client'
import { logger } from '@/lib/logger'
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public type: string = 'server_error'
) {
super(message)
}
}
export function errorHandler(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
// Zod validation error
if (error instanceof ZodError) {
return res.status(400).json({
message: 'Validation error',
type: 'validation_error',
errors: error.errors.map(e => ({
path: e.path.join('.'),
message: e.message,
})),
})
}
// Prisma errors
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return res.status(409).json({
message: 'Resource already exists',
type: 'conflict_error',
})
}
if (error.code === 'P2025') {
return res.status(404).json({
message: 'Resource not found',
type: 'not_found_error',
})
}
}
// App errors (custom)
if (error instanceof AppError) {
return res.status(error.statusCode).json({
message: error.message,
type: error.type,
})
}
// Unknown errors
logger.error('Unexpected error:', error)
return res.status(500).json({
message: 'Internal server error',
type: 'server_error',
})
}
```
---
### 7. Validation Middleware (Zod)
```typescript
// src/middleware/validation.ts
import { Request, Response, NextFunction } from 'express'
import { AnyZodObject, ZodError } from 'zod'
export function validateRequest(schema: AnyZodObject) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
})
next()
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
message: 'Validation error',
type: 'validation_error',
errors: error.errors.map(e => ({
path: e.path.join('.'),
message: e.message,
})),
})
}
next(error)
}
}
}
```
---
### 8. Server Entry Point
```typescript
// src/server.ts
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import { env } from '@/config/env'
import { errorHandler } from '@/middleware/error'
import { logger } from '@/lib/logger'
import router from '@/routes'
const app = express()
// Security middleware
app.use(helmet())
app.use(cors({ origin: env.CORS_ORIGIN }))
app.use(express.json({ limit: '10mb' }))
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
// API routes
app.use('/api', router)
// Error handler (MUST be last)
app.use(errorHandler)
// Start server
const server = app.listen(env.PORT, () => {
logger.info(`Server running on port ${env.PORT}`)
logger.info(`Environment: ${env.NODE_ENV}`)
})
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully')
server.close(() => {
logger.info('Server closed')
process.exit(0)
})
})
export default app
```
---
### 9. TypeScript Config (Strict Mode)
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
---
## Naming Conventions
### Variables & Functions (camelCase)
```typescript
const userId = "123"
const currentUser = await getUser()
async function createTask() {}
async function getUserTasks() {}
```
### Classes & Types (PascalCase)
```typescript
class TaskService {}
type TaskResponse = {}
interface UserData {}
```
### Files (kebab-case)
```
task.service.ts
user.controller.ts
auth.middleware.ts
```
### Constants (SCREAMING_SNAKE_CASE)
```typescript
const MAX_TASKS_PER_USER = 100
const DEFAULT_PAGE_SIZE = 20
```
---
## Anti-Patterns à Éviter
❌ **Multiple PrismaClient instances**
```typescript
// ❌ INTERDIT
const prisma = new PrismaClient() // Dans chaque fichier
```
✅ **Singleton pattern**
```typescript
// ✅ CORRECT
import { prisma } from '@/lib/prisma'
```
---
❌ **Business logic dans routes**
```typescript
// ❌ INTERDIT
router.post('/', async (req, res) => {
const task = await prisma.task.create({ data: req.body })
res.json(task)
})
```
✅ **Business logic dans services**
```typescript
// ✅ CORRECT
router.post('/', async (req, res) => {
const task = await taskService.createTask(req.user.id, req.body)
res.json(task)
})
```
---
❌ **Pas de validation**
```typescript
// ❌ INTERDIT
router.post('/', async (req, res) => {
const task = await taskService.createTask(req.body) // Pas de validation
})
```
✅ **Zod validation middleware**
```typescript
// ✅ CORRECT
router.post('/', validateRequest(createTaskSchema), async (req, res) => {
const task = await taskService.createTask(req.body)
})
```
---
❌ **Errors non catchés**
```typescript
// ❌ INTERDIT
router.get('/', async (req, res) => {
const tasks = await taskService.getTasks() // Crash si erreur
})
```
✅ **Try/catch + next(error)**
```typescript
// ✅ CORRECT
router.get('/', async (req, res, next) => {
try {
const tasks = await taskService.getTasks()
res.json(tasks)
} catch (error) {
next(error) // Error handler middleware
}
})
```
---
## Checklist Feature API
**Avant créer route:**
- [ ] Service existe? (anti-duplication)
- [ ] Zod schema défini?
- [ ] Auth middleware si protected?
- [ ] Error handling (try/catch + next)?
**Après créer route:**
- [ ] Route exportée dans `routes/index.ts`?
- [ ] Types TypeScript définis?
- [ ] Ownership checks (si user data)?
---
## Exemples Complets
### Exemple 1: CRUD Tasks API
**Structure créée:**
```
src/
├── routes/tasks.ts # Routes HTTP
├── services/task.service.ts # Business logic
├── types/task.ts # TypeScript types
```
**Workflow:**
1. Zod schema validation
2. Route appelle service
3. Service utilise Prisma
4. Error handling middleware
---
### Exemple 2: Auth Middleware
```typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { env } from '@/config/env'
import { AppError } from '@/middleware/error'
declare global {
namespace Express {
interface Request {
user: { id: string; email: string }
}
}
}
export async function requireAuth(
req: Request,
res: Response,
next: NextFunction
) {
try {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
throw new AppError('Authentication required', 401, 'auth_error')
}
const decoded = jwt.verify(token, env.JWT_SECRET) as {
id: string
email: string
}
req.user = decoded
next()
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
return next(new AppError('Invalid token', 401, 'auth_error'))
}
next(error)
}
}
```
---
## Workflow Executor avec Backend-Node Skill
**Détection auto:**
```
1. executor scan projet
2. Trouve package.json + src/server.ts
3. Load backend-nodejs skill (au lieu de backend Python)
4. Applique conventions Node.js/TypeScript
```
**Feature création:**
```
User: "Crée CRUD tasks API"
executor:
1. Load skills: backend-nodejs + frontend + integration
2. Crée structure selon conventions Node.js
3. Zod validation + Prisma + Express routes
4. Services singleton pattern
5. Error handling standardisé
```
---
## Principes
1. **TypeScript Strict Mode** - Zéro `any`, types partout
2. **Configuration Centralisée** - 1 seul `env.ts` (12-Factor App)
3. **Services Singleton** - 1 service par ressource, réutilisable
4. **Validation Zod** - Toutes inputs validées
5. **Error Handling Standardisé** - Errors structurés + types
6. **Prisma Best Practices** - Singleton client, select optimization
7. **Separation of Concerns** - Routes → Services → Prisma
**Inspiré de:**
- Vercel API Routes design
- Stripe backend architecture (services pattern)
- Prisma documentation (singleton, transactions)
- Express.js best practices
---
**Version**: 1.0.0
**Created**: 2025-01-10
**Maintained by**: executor agent (loaded si projet Node.js détecté)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!