node-writing-code

SKILL.md

Node.js Writing Code

Quick reference for writing production-quality Node.js backend services. Each section summarizes the key rules — reference files provide full examples and edge cases.

API Architecture (Fastify)

Plugin-Based Design

Everything in Fastify is a plugin — routes, middleware, database connections, services.

// Register plugins in order
await app.register(cors, { origin: ["http://localhost:3000"], credentials: true });
await app.register(jwt, { secret: process.env.JWT_SECRET! });

// Route plugins with prefix
await app.register(authRoutes, { prefix: "/auth" });
await app.register(userRoutes, { prefix: "/users" });

Route Organization

src/
├── routes/           # Route plugins grouped by resource
│   ├── auth/
│   ├── users/
│   └── appointments/
├── plugins/          # Cross-cutting (database, auth decorator)
├── services/         # Business logic
├── schemas/          # Shared Zod/JSON schemas
└── app.ts            # App factory

Route Definition

fastify.post("/appointments", {
  schema: { body: CreateAppointmentSchema, response: { 201: AppointmentSchema } },
  preHandler: [fastify.authenticate],
  handler: async (request, reply) => {
    const data = request.body as CreateAppointmentInput;
    const appointment = await appointmentService.create(data);
    return reply.code(201).send(appointment);
  },
});

Hooks (Middleware)

onRequest → preParsing → preValidation → preHandler → handler → preSerialization → onSend → onResponse

Use preHandler for auth, validation, rate limiting. Use onRequest for logging.

See references/api-patterns.md for plugin architecture, route patterns, error handling, decorators, and OpenAPI integration.

Validation (Zod)

Rules

  1. Validate at boundaries — HTTP handlers, environment variables, external API responses
  2. Trust internal code — Don't re-validate data that's already been validated
  3. Parse, don't validate — Use safeParse to transform and validate in one step
  4. Derive types from schemastype User = z.infer<typeof UserSchema>
const CreateUserSchema = z.object({
  email: z.string().email("Correo no válido"),
  name: z.string().min(1, "Nombre requerido"),
  password: z.string().min(8, "Mínimo 8 caracteres"),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

// In handler
const result = CreateUserSchema.safeParse(request.body);
if (!result.success) {
  return reply.code(400).send({ error: "Validation Error", details: result.error.flatten().fieldErrors });
}
const user = await userService.create(result.data);

Environment Validation

const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
});

export const env = EnvSchema.parse(process.env); // Fail fast at startup

See references/validation.md for schema composition, query/path params, error formatting, and anti-patterns.

Database Access

Prisma

// Schema-first: define models in schema.prisma, generate client
const user = await prisma.user.findUnique({ where: { id }, include: { profile: true } });

// Transactions
const [user, appointment] = await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({ data: userData });
  const apt = await tx.appointment.create({ data: { ...aptData, userId: user.id } });
  return [user, apt];
});

Drizzle

// Schema-as-code: define tables in TypeScript
const users = await db.select().from(schema.users)
  .where(eq(schema.users.role, "doctor"))
  .orderBy(schema.users.name)
  .limit(20);

Key Rules

  • Prevent N+1 — Use include (Prisma) or joins (Drizzle) instead of loops
  • Select only needed fields — Don't SELECT *
  • Use cursor pagination for large datasets
  • Index frequently queried columns — Composite indexes for filtered queries
  • Singleton client — Share one database client across the app
  • Use migrations — Never push schema changes directly in production

See references/database.md for CRUD operations, migrations, transactions, connection management, and optimization.

Authentication & Authorization

JWT Token Strategy

  • Access token: 15 min, stateless, in Authorization: Bearer header
  • Refresh token: 7 days, stored in database, rotated on use
// Auth middleware
fastify.decorate("authenticate", async (request, reply) => {
  const token = request.headers.authorization?.slice(7); // Remove "Bearer "
  if (!token) return reply.code(401).send({ error: "Missing token" });

  try {
    request.user = verifyAccessToken(token);
  } catch {
    return reply.code(401).send({ error: "Invalid token" });
  }
});

RBAC

function requireRole(...roles: Role[]) {
  return async (request: FastifyRequest, reply: FastifyReply) => {
    await request.server.authenticate(request, reply);
    if (!roles.includes(request.user.role as Role)) {
      return reply.code(403).send({ error: "Insufficient permissions" });
    }
  };
}

// Usage
fastify.delete("/users/:id", { preHandler: [requireRole("admin")] }, handler);

ABAC

For fine-grained access based on resource attributes:

function canAccessAppointment(auth: AuthContext, appointment: Appointment): boolean {
  if (auth.role === "admin") return true;
  if (auth.role === "patient" && appointment.userId === auth.userId) return true;
  if (auth.role === "doctor" && appointment.doctorId === auth.userId) return true;
  return false;
}

Security Rules

  • Bcrypt with cost factor 12+ for passwords
  • Rotate refresh tokens on each use
  • Rate limit auth endpoints
  • Log auth events (without passwords)

See references/auth.md for login flow, token refresh, OAuth 2.0, password hashing, and security best practices.

Error Handling

Global Error Handler

app.setErrorHandler((error, request, reply) => {
  request.log.error(error);

  if (error.validation) {
    return reply.code(400).send({ error: "Validation Error", details: error.validation });
  }
  if (error.statusCode) {
    return reply.code(error.statusCode).send({ error: error.name, message: error.message });
  }
  return reply.code(500).send({ error: "Internal Server Error" });
});

Custom Errors

import createError from "@fastify/error";

const NotFoundError = createError("NOT_FOUND", "Resource %s not found", 404);
const ConflictError = createError("CONFLICT", "%s already exists", 409);

// Usage
throw new NotFoundError("User");

HTTP Status Codes

Code Meaning Use When
200 OK Successful GET, PUT
201 Created Successful POST that creates a resource
204 No Content Successful DELETE
400 Bad Request Validation errors
401 Unauthorized Missing or invalid auth token
403 Forbidden Valid auth, insufficient permissions
404 Not Found Resource doesn't exist
409 Conflict Duplicate resource (e.g., email taken)
500 Internal Error Unexpected server errors

Docker

Multi-Stage Build

Three stages: deps (prod only) → build (compile) → production (minimal runtime).

FROM node:22-alpine AS production
RUN addgroup -S nodejs && adduser -S nodejs
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER nodejs
CMD ["node", "dist/index.js"]

Key Rules

  • Non-root user — Always USER nodejs
  • Health checks/health, /health/live, /health/ready endpoints
  • Graceful shutdown — Handle SIGTERM and SIGINT, close connections
  • No secrets in images — Inject via environment variables at runtime
  • Alpine base — Smallest image size
  • Pin versionsnode:22-alpine not node:latest

See references/docker.md for full Dockerfile, Docker Compose, health checks, graceful shutdown, and production optimization.

Post-Change Verification

After every Node.js code change, run the TypeScript verification protocol from the typescript-writing-code skill:

pnpm run type-check && pnpm run lint && pnpm run format && pnpm run test

All 4 steps must pass. See typescript-writing-code skill for details.

Reference Files

File Description
references/api-patterns.md Fastify plugins, routes, hooks, error handling, OpenAPI, decorators
references/validation.md Zod schemas, API request validation, env validation, error formatting
references/database.md Prisma, Drizzle, migrations, query optimization, N+1 prevention
references/auth.md JWT, sessions, OAuth, RBAC, ABAC, permission guards
references/docker.md Multi-stage builds, security, health checks, graceful shutdown
Weekly Installs
2
GitHub Stars
2
First Seen
4 days ago
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2