backend-nodejs
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.jsonexiste - Fichiers
server.tsouindex.tsousrc/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)
// 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)
// 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)
// 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)
// 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é)
// 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)
// 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
// 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)
// 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)
const userId = "123"
const currentUser = await getUser()
async function createTask() {}
async function getUserTasks() {}
Classes & Types (PascalCase)
class TaskService {}
type TaskResponse = {}
interface UserData {}
Files (kebab-case)
task.service.ts
user.controller.ts
auth.middleware.ts
Constants (SCREAMING_SNAKE_CASE)
const MAX_TASKS_PER_USER = 100
const DEFAULT_PAGE_SIZE = 20
Anti-Patterns à Éviter
❌ Multiple PrismaClient instances
// ❌ INTERDIT
const prisma = new PrismaClient() // Dans chaque fichier
✅ Singleton pattern
// ✅ CORRECT
import { prisma } from '@/lib/prisma'
❌ Business logic dans routes
// ❌ INTERDIT
router.post('/', async (req, res) => {
const task = await prisma.task.create({ data: req.body })
res.json(task)
})
✅ Business logic dans services
// ✅ CORRECT
router.post('/', async (req, res) => {
const task = await taskService.createTask(req.user.id, req.body)
res.json(task)
})
❌ Pas de validation
// ❌ INTERDIT
router.post('/', async (req, res) => {
const task = await taskService.createTask(req.body) // Pas de validation
})
✅ Zod validation middleware
// ✅ CORRECT
router.post('/', validateRequest(createTaskSchema), async (req, res) => {
const task = await taskService.createTask(req.body)
})
❌ Errors non catchés
// ❌ INTERDIT
router.get('/', async (req, res) => {
const tasks = await taskService.getTasks() // Crash si erreur
})
✅ Try/catch + next(error)
// ✅ 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:
- Zod schema validation
- Route appelle service
- Service utilise Prisma
- Error handling middleware
Exemple 2: Auth Middleware
// 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
- TypeScript Strict Mode - Zéro
any, types partout - Configuration Centralisée - 1 seul
env.ts(12-Factor App) - Services Singleton - 1 service par ressource, réutilisable
- Validation Zod - Toutes inputs validées
- Error Handling Standardisé - Errors structurés + types
- Prisma Best Practices - Singleton client, select optimization
- 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é)
More from diegosouzapw/awesome-omni-skill
music-assistant
Control Home Assistant Music Assistant - browse library, search, play, manage preferences and moods.
12agent-code-generator
Generates Agent definitions (.md files) based on user intent and standard templates.
6terragrunt-generator
Comprehensive toolkit for generating best practice Terragrunt configurations (HCL files) following current standards and conventions. Use this skill when creating new Terragrunt resources (root configs, child modules, stacks, environment setups), or building multi-environment Terragrunt projects.
6api contract sync manager
Validate OpenAPI, Swagger, and GraphQL schemas match backend implementation. Detect breaking changes, generate TypeScript clients, and ensure API documentation stays synchronized. Use when working with API spec files (.yaml, .json, .graphql), reviewing API changes, generating frontend types, or validating endpoint implementations.
5upstash/workflow typescript sdk skill
Lightweight guidance for using the Upstash Workflow SDK to define, trigger, and manage workflows. Use this Skill whenever a user wants to create workflow endpoints, run steps, or interact with the Upstash Workflow client.
5upstash/search typescript sdk
Entry point for documentation skills covering Upstash Search quick starts, core concepts, and TypeScript SDK usage. Use when a user asks how to get started, how indexing works, or how to use the TS client.
5