How to Run SQL Query in TypeScript: Complete Guide with Examples
Executive Summary
Over 49% of developers use TypeScript for backend development, making SQL integration a critical skill for modern applications.
This guide covers the essential approaches, common pitfalls, and production-ready patterns you should know. Last verified: April 2026. We’ll walk through multiple database libraries, error handling strategies, and real-world scenarios you’ll encounter. The key to success is choosing the right library for your use case, implementing proper error handling, and always closing database connections—even when things go wrong.
Learn TypeScript on Udemy
Main Data Table
| Aspect | Details |
|---|---|
| Action | Run SQL Query |
| Language | TypeScript |
| Difficulty Level | Intermediate |
| Primary Consideration | Correctness, Performance, Error Handling |
| Key Resources | TypeScript standard library, third-party database drivers |
| Critical Skill | Resource management (connection closing) |
Breakdown by Implementation Approach
Different scenarios call for different SQL execution patterns in TypeScript. Here’s how the main approaches break down:
| Approach | Best For | Complexity |
|---|---|---|
| Raw Driver (pg, mysql2) | Fine-grained control, performance-critical code | Higher |
| Query Builders (Knex.js) | Dynamic SQL generation, cleaner syntax | Medium |
| ORMs (TypeORM, Prisma) | Rapid development, type safety, relationships | Lower |
| Stored Procedures | Complex business logic, legacy systems | Variable |
Method Comparison: SQL Execution Patterns
When executing SQL in TypeScript, you have several battle-tested approaches. Here’s how they compare:
| Pattern | Performance | Type Safety | Learning Curve | Flexibility |
|---|---|---|---|---|
| Direct Driver (pg) | Excellent | Manual | Steep | Maximum |
| Knex.js Query Builder | Very Good | Partial | Moderate | High |
| TypeORM ORM | Good | Excellent | Moderate | Medium |
| Prisma ORM | Good | Excellent | Easy | Medium |
| Raw SQL with Parameterization | Excellent | Manual | Easy | Maximum |
Key Factors for Running SQL Queries in TypeScript
1. Parameterization and SQL Injection Prevention
Never concatenate user input directly into SQL strings. Always use parameterized queries (prepared statements). This is non-negotiable for security. When you use parameters, the database driver handles escaping and treats user input as data, not executable code.
// ❌ DANGEROUS - SQL injection risk
const userId = req.query.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
const result = await client.query(query);
// ✅ SAFE - Parameterized query
const userId = req.query.id;
const query = 'SELECT * FROM users WHERE id = $1';
const result = await client.query(query, [userId]);
2. Connection Pool Management
Creating a new database connection for every query is wasteful. Connection pools maintain a set of reusable connections, dramatically improving performance in production. Most libraries handle this automatically, but you need to configure pool size appropriately.
import { Pool } from 'pg';
const pool = new Pool({
user: 'postgres',
password: process.env.DB_PASSWORD,
host: 'localhost',
port: 5432,
database: 'myapp',
max: 20, // Maximum pool size
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Queries automatically use pooled connections
const result = await pool.query('SELECT * FROM users WHERE id = $1', [1]);
3. Error Handling and Resource Cleanup
Database operations fail. Network timeouts happen. Your code must handle these gracefully and always release resources, even during errors. Use try-catch-finally blocks or async context managers to ensure cleanup occurs.
let client;
try {
client = await pool.connect();
const result = await client.query('SELECT * FROM users');
return result.rows;
} catch (error) {
console.error('Database query failed:', error);
throw new Error(`Failed to fetch users: ${error.message}`);
} finally {
if (client) {
client.release();
}
}
4. Type Safety with TypeScript
Leverage TypeScript’s type system to catch errors at compile-time rather than runtime. Define interfaces for your query results and function return types. Some libraries like Prisma generate types automatically from your database schema.
interface User {
id: number;
email: string;
created_at: Date;
}
async function getUserById(id: number): Promise {
const result = await pool.query(
'SELECT id, email, created_at FROM users WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
5. Async/Await Patterns and Promise Handling
Database queries are I/O operations—always async. Use async/await for readability and proper error propagation. Avoid callback hell and ensure you’re not blocking the event loop with synchronous operations.
// ✅ Correct: Using async/await
async function fetchUserData(userId: number) {
try {
const user = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
const posts = await pool.query('SELECT * FROM posts WHERE user_id = $1', [userId]);
return { user: user.rows[0], posts: posts.rows };
} catch (error) {
console.error('Query failed:', error);
throw error;
}
}
Historical Trends in SQL Query Execution
The landscape of database interaction in TypeScript has evolved significantly. In the early days (2015-2017), developers primarily used raw driver libraries like pg or mysql. This required manual connection management and heavy use of callbacks.
Query builders like Knex.js emerged around 2015-2016, offering a middle ground between raw SQL and full ORMs. They gained traction for their flexibility and ability to generate complex queries programmatically.
The real shift happened around 2018-2020 with TypeScript-native ORMs. TypeORM (2016) pioneered decorator-based entity definitions, and Prisma (2019) introduced the concept of a schema file and auto-generated, type-safe client. By 2024-2026, Prisma became the default choice for new TypeScript projects because it eliminates entire categories of bugs through its type-first approach.
We’re now seeing a pendulum swing back toward lightweight patterns—developers appreciate Prisma’s DX but value the performance and simplicity of parameterized raw SQL combined with basic connection pooling. The consensus: choose based on your project scope. Startups favor Prisma; performance-critical systems still use raw drivers with query builders.
Expert Tips for Running SQL Queries in TypeScript
1. Always Use Prepared Statements or Parameterized Queries
This prevents SQL injection, improves performance through query plan caching, and keeps your code clean. Every major TypeScript database library supports this—use it without exception.
2. Implement Exponential Backoff for Transient Failures
Network hiccups happen. Wrap critical queries in retry logic with exponential backoff to handle temporary database unavailability gracefully:
async function queryWithRetry(fn: () => Promise, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
const result = await queryWithRetry(() =>
pool.query('SELECT * FROM users WHERE id = $1', [userId])
);
3. Use Database Transactions for Multi-Step Operations
When multiple queries must succeed or fail together, use transactions. They guarantee consistency and prevent partial updates:
async function transferFunds(fromUserId: number, toUserId: number, amount: number) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - $1 WHERE user_id = $2', [amount, fromUserId]);
await client.query('UPDATE accounts SET balance = balance + $1 WHERE user_id = $2', [amount, toUserId]);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
4. Monitor Query Performance with Logging
Add query execution time logging to identify slow queries before they become production problems. Many ORMs have built-in logging; configure it appropriately for your environment.
5. Cache Query Results Thoughtfully
Not every query needs caching, but frequently accessed, rarely-changing data (like user preferences or configuration) benefits significantly. Use Redis or in-memory caches with appropriate TTLs to reduce database load.
FAQ: Running SQL Queries in TypeScript
1. What’s the fastest way to run SQL queries in TypeScript?
Raw parameterized queries using the pg library for PostgreSQL offer the best performance. Direct driver access eliminates the abstraction overhead of ORMs. However, for most applications, the performance difference is negligible compared to the developer productivity gains from using Prisma or TypeORM. Profile your actual workload before optimizing prematurely.
2. How do I prevent SQL injection in TypeScript?
Always use parameterized queries (also called prepared statements). Replace dynamic values with placeholders ($1, $2, ?, etc.) and pass the values as separate parameters. The database driver handles proper escaping. For example: pool.query('SELECT * FROM users WHERE id = $1', [userId]). Never concatenate user input directly into SQL strings.
3. Should I use an ORM like Prisma or stick with raw SQL?
Use Prisma or TypeORM for new projects because they provide excellent type safety, eliminate boilerplate, and prevent common mistakes. Use raw SQL (with a query builder or driver) when you need maximum performance, are working with legacy systems, or have highly specialized queries. Many teams use both—ORMs for standard CRUD, raw SQL for complex reporting queries.
4. What happens if a query hangs indefinitely?
Set query timeouts at the pool level to prevent hanging connections from exhausting your pool. Most drivers support timeout configuration. Additionally, implement application-level timeouts using Promise.race() for critical queries that must complete within a specific timeframe.
5. How many connections should my pool maintain?
A good starting point is 10-20 connections for most applications. Monitor your actual connection usage under load. Too few connections cause queuing; too many waste memory and strain the database. For serverless environments (Lambda, Vercel Functions), use smaller pools (2-5) because each invocation gets its own runtime.
Conclusion
Running SQL queries in TypeScript successfully boils down to three fundamentals: use parameterized queries to prevent injection attacks, manage connections through pooling to maximize performance, and handle errors gracefully with proper try-catch-finally blocks.
Choose your approach based on your project’s requirements. Prisma or TypeORM for most modern applications. Raw drivers or query builders when you need maximum control. Always prioritize security over convenience—parameterized queries take the same amount of code as concatenated SQL, so there’s no excuse for raw string queries.
Start with a simple example using whichever library you choose, test it thoroughly with edge cases (empty results, null values, connection failures), and gradually build your confidence with more complex queries and transactions. The patterns you learn here will serve you across any TypeScript project that touches a database.
Learn TypeScript on Udemy
Related tool: Try our free calculator