Always use parameterized queries or prepared statements, never string concatenation. String-interpolated SQL is the #1 cause of SQL injection — an attacker can modify your query to read, modify, or delete your entire database.
Why This Matters
SQL injection has been the #1 web vulnerability for over 20 years. When user input is concatenated into SQL strings, an attacker can terminate the intended query and inject their own SQL. This allows them to bypass authentication, read any table, modify any data, or drop the entire database. Parameterized queries prevent this by separating the SQL structure from the data values, making injection impossible by design.
SQL injection occurs when user input is embedded directly into a SQL query string. The database cannot distinguish between the query structure and the user-supplied data, so an attacker can modify the query itself.
Consider a login query: SELECT * FROM users WHERE email = '${email}'. If the attacker submits ' OR 1=1 -- as the email, the query becomes:
SELECT * FROM users WHERE email = '' OR 1=1 --'
This returns every user in the table, bypassing authentication. More destructive payloads can DROP TABLE, INSERT admin users, or COPY data to external servers.
Parameterized queries solve this by sending the query structure and data values separately. The database treats parameters as data, never as SQL structure, making injection impossible by design.
The rule
Never concatenate, interpolate, or template user input into SQL strings. Always use parameterized queries (also called prepared statements or bind variables) where the SQL structure and data values are separated.
Bad example
// BAD: string interpolation — SQL injection vulnerabilityasync function getUser(email: string) { const result = await pool.query( `SELECT * FROM users WHERE email = '${email}'` ); return result.rows[0];}// BAD: template literal — also vulnerableasync function searchProducts(name: string) { const result = await pool.query( `SELECT * FROM products WHERE name LIKE '%${name}%'` ); return result.rows;}
Good example
// GOOD: parameterized query — injection impossibleasync function getUser(email: string) { const result = await pool.query( "SELECT * FROM users WHERE email = $1", [email] ); return result.rows[0];}// GOOD: parameterized LIKE queryasync function searchProducts(name: string) { const result = await pool.query( "SELECT * FROM products WHERE name LIKE $1", [`%${name}%`] ); return result.rows;}// GOOD: using an ORM (Supabase, Prisma, Drizzle) — automatically parameterizedconst { data } = await supabase .from("users") .select("*") .eq("email", email);