Preventing SQL Injection in Next.js

A comprehensive developer guide to preventing SQL injection in Next.js App Router, Server Actions, Route Handlers, and modern ORMs.

Abstract Next.js server actions and database boundary architecture with red defensive filtering motifs

With the adoption of the Next.js App Router and React Server Components (RSC), full-stack developers can query databases directly from server-side components and Server Actions. While this eliminates boilerplate REST API controllers, it shifts the database security boundary directly into the application layer. Without proper parameterized access, applications expose critical database infrastructure to injection attacks.

To grasp the baseline theory of query manipulation, review our foundational guide on how SQL injection works and why it remains dangerous. If you are building standalone Express microservices, also consult our tutorial on preventing SQL injection in Node.js and Express.

In this guide, we break down SQL injection prevention across Next.js Server Components, Server Actions, Route Handlers, and modern TypeScript ORMs.

Where SQL injection risks occur in the Next.js App Router

In Next.js, untrusted client inputs enter the server through three distinct mechanisms:

  1. Server Actions ('use server'): Form submissions and client component invocations passing serialized arguments.
  2. Route Handlers (app/api/.../route.ts): Traditional HTTP request payloads (request.json() or search parameters).
  3. Server Component Props: URL search parameters passed into PageProps ({ searchParams }).

A vulnerability arises when any of these inputs are formatted directly into SQL query strings:

// DANGEROUS: Server Action with string interpolation
'use server';

import { db } from '@/lib/db';

export async function filterUsers(department: string) {
  // Vulnerable to SQL injection via client form tampering
  const query = `SELECT id, name, role FROM employees WHERE department = '${department}'`;
  const result = await db.query(query);
  return result.rows;
}

Because Server Actions can be invoked via HTTP POST requests from arbitrary origins or script tools, relying solely on client-side React input validation provides zero security.

Securing Server Actions with input validation and parameterization

To eliminate SQL injection in Server Actions, implement a two-step defense: validate input schemas using Zod, and execute queries using strict parameter bindings:

// SECURE: Server Action with Zod validation and parameterized query
'use server';

import { z } from 'zod';
import pool from '@/lib/db';

const SearchSchema = z.object({
  department: z.string().trim().min(1).max(50),
  limit: z.number().int().positive().max(100).default(20)
});

export async function searchEmployeesAction(formData: FormData) {
  const parsed = SearchSchema.safeParse({
    department: formData.get('department'),
    limit: Number(formData.get('limit') || 20)
  });

  if (!parsed.success) {
    return { error: 'Invalid input parameters', data: [] };
  }

  const { department, limit } = parsed.data;

  // Parameterized query: $1 and $2 are evaluated as literal constants
  const query = `
    SELECT id, name, department, role 
    FROM employees 
    WHERE department = $1 
    ORDER BY id ASC 
    LIMIT $2
  `;

  try {
    const { rows } = await pool.query(query, [department, limit]);
    return { error: null, data: rows };
  } catch (err) {
    console.error('Database query error:', err);
    return { error: 'Failed to retrieve records', data: [] };
  }
}

Route Handlers: Parameterizing dynamic URL search params

In Next.js Route Handlers (route.ts), query parameters obtained from req.nextUrl.searchParams must be treated as untrusted data:

// SECURE: app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const category = searchParams.get('category');
  const inStock = searchParams.get('inStock') === 'true';

  if (!category) {
    return NextResponse.json({ error: 'Category is required' }, { status: 400 });
  }

  // Prepared statement syntax with parameterized values
  const text = 'SELECT id, title, price FROM products WHERE category = $1 AND in_stock = $2';
  const values = [category, inStock];

  const { rows } = await pool.query(text, values);
  return NextResponse.json({ products: rows });
}

Modern TypeScript ORMs in Next.js: Drizzle and Prisma

Using an ORM is the most common paradigm in modern Next.js development. Modern ORMs generate parameterized queries under the hood, but developers must understand when raw extensions introduce risks.

Secure queries with Drizzle ORM

Drizzle enforces type-safe parameterization through its SQL template tag (sql):

import { db } from '@/lib/drizzle';
import { users } from '@/lib/schema';
import { eq, sql } from 'drizzle-orm';

// SECURE: Native Drizzle query builder
export async function getActiveUsers(roleName: string) {
  return db.select()
    .from(users)
    .where(eq(users.role, roleName));
}

// SECURE: Drizzle sql template tag automatically parameterizes expressions
export async function customFilter(userInput: string) {
  return db.execute(sql`
    SELECT id, email, created_at 
    FROM ${users} 
    WHERE status = ${userInput}
  `);
}

// DANGEROUS: sql.raw disables parameterization
// db.execute(sql.raw(`SELECT * FROM users WHERE status = '${userInput}'`)); // NEVER DO THIS

Secure queries with Prisma in Next.js

import { prisma } from '@/lib/prisma';

// SECURE: Prisma type-safe query
export async function findUserByEmail(email: string) {
  return prisma.user.findUnique({
    where: { email }
  });
}

// SECURE: $queryRaw tagged template automatically parameterizes inputs
export async function rawReport(companyId: string) {
  return prisma.$queryRaw`
    SELECT id, total_revenue 
    FROM financial_reports 
    WHERE company_id = ${companyId}
  `;
}

Common Next.js database pitfalls and mitigations

Pitfall Security Risk Recommended Mitigation
Exposing Raw SQL in Server Actions Unauthenticated callers invoke actions with malicious strings Enforce Zod input schemas and server session authentication
Trusting SearchParams in Server Components Attacker crafts malicious URLs leading to blind SQLi Parameterize all database calls inside page.tsx loaders
Using sql.raw or $queryRawUnsafe Completely bypasses ORM parameterization Restrict raw SQL to tagged template literals (sql\...``)
Leaking Database Stack Traces in Development Error boundaries return sensitive table schemas to clients Return opaque error messages in production build configurations

To establish structured team conventions that catch these architectural vulnerabilities early, follow our clean code and code review habits guide.

Security checklist for Next.js database operations

Audit your Next.js application against this checklist prior to production deployment:

  • Every Server Action verifies caller authentication and validates arguments with Zod schemas.
  • No database operations concatenate searchParams or form inputs directly into query strings.
  • Raw queries in Prisma or Drizzle use tagged templates ($queryRaw or `sql``) exclusively.
  • Environment variables for database credentials (DATABASE_URL) are strictly kept server-side (no NEXT_PUBLIC_ prefix).
  • Automated security linting rules detect any occurrences of $queryRawUnsafe in the codebase.

Bottom line

The React Server Components and Server Actions model in Next.js provides incredible developer ergonomics. By ensuring that every database interaction relies on parameterized placeholders, validating inputs with strict schemas, and avoiding raw string concatenation, you can leverage Next.js without sacrificing database security.

Sources: Next.js Server Actions Security Architecture, Drizzle ORM Documentation on SQL Operators, Prisma Documentation on Raw Queries, OWASP Top 10 (evaluated November 2026).

Keep reading