Skip to main content
Web DevelopmentSoftware Engineering

Securing Your Node.js API: A Checklist for Production

A comprehensive security checklist covering authentication, input validation, rate limiting, and common vulnerabilities in Node.js APIs.

Mbeah Essilfie

Mbeah Essilfie

July 7, 2026 at 06:03 AM

11 min read 2146 views
Securing Your Node.js API: A Checklist for Production

Security Is Not Optional

Every API deployed to production is a target. This isn't about paranoia — it's about building habits that prevent breaches.

1. Input Validation

Never trust user input. Validate everything at the boundary:

import { z } from 'zod'

const UserInputSchema = z.object({
  email: z.string().email().max(254),
  name: z.string().min(1).max(100).trim(),
  age: z.number().int().min(13).max(150),
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const validated = UserInputSchema.parse(body) // Throws on invalid input
  // Use validated data — it's safe
})

2. Authentication & Authorization

// Middleware: verify JWT on protected routes
export default defineEventHandler(async (event) => {
  const token = getHeader(event, 'Authorization')?.replace('Bearer ', '')
  if (!token) throw createError({ statusCode: 401, message: 'Unauthorized' })

  try {
    const payload = verifyJWT(token)
    event.context.user = payload
  } catch {
    throw createError({ statusCode: 401, message: 'Invalid token' })
  }
})
Authentication answers "who are you?" — Authorization answers "what can you do?" Both are required. Never skip authorization checks just because a user is authenticated.

3. Rate Limiting

// Simple in-memory rate limiter (use Redis for production clusters)
const requests = new Map<string, { count: number; resetAt: number }>()

function rateLimit(ip: string, limit = 100, windowMs = 60000): boolean {
  const now = Date.now()
  const record = requests.get(ip)

  if (!record || now > record.resetAt) {
    requests.set(ip, { count: 1, resetAt: now + windowMs })
    return true
  }

  if (record.count >= limit) return false
  record.count++
  return true
}

4. SQL Injection Prevention

Always use parameterized queries (Prisma does this by default):

// ❌ NEVER do this
const user = await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = '${userId}'`)

// ✅ Always use parameterized queries
const user = await prisma.user.findUnique({ where: { id: userId } })

5. Security Headers

// server/middleware/security-headers.ts
export default defineEventHandler((event) => {
  setHeaders(event, {
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'X-XSS-Protection': '1; mode=block',
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    'Content-Security-Policy': "default-src 'self'",
  })
})

6. Environment Variables

  • Never commit secrets to git
  • Use different secrets per environment
  • Rotate secrets periodically
  • Audit access to secret stores

The Checklist

  • All inputs validated with strict schemas
  • Authentication on every protected route
  • Authorization checks at the resource level
  • Rate limiting on public endpoints
  • Security headers configured
  • HTTPS enforced
  • Dependencies audited regularly (pnpm audit)
  • Error messages don't leak internals
Node.jsSecurityAPI Design
Mbeah Essilfie

Written by Mbeah Essilfie

Fullstack Software Developer

Read next

The Complete Guide to Vue 3 Composables
12 min read

The Complete Guide to Vue 3 Composables

Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.

Mbeah EssilfieMbeah Essilfie
989