Prerequisites
- Basic understanding of JavaScript π
- TypeScript installation β‘
- VS Code or preferred IDE π»
What you'll learn
- Understand the concept fundamentals π―
- Apply the concept in real projects ποΈ
- Debug common issues π
- Write type-safe code β¨
π PostgreSQL with TypeScript: SQL Database Magic! π
Welcome to the awesome world of PostgreSQL with TypeScript! π If youβve ever dreamed of having a database thatβs both powerful and type-safe, youβre in for a treat! Think of PostgreSQL as your reliable friend who never forgets anything π§ , and TypeScript as your helpful assistant who makes sure you never make silly mistakes π‘οΈ.
Ready to build some amazing database-powered applications? Letβs dive in! πͺ
π― Introduction
Imagine youβre building the next big social media platform π±, and you need to store millions of users, posts, and comments. You want something thatβs:
β
Super reliable - No data loss, ever! π
β
Lightning fast - Even with millions of records β‘
β
Type-safe - Catch errors before they reach production π
β
Scalable - Grows with your success π
Thatβs exactly what PostgreSQL + TypeScript gives you! Itβs like having a superhero database team π¦ΈββοΈπ¦ΈββοΈ.
π Understanding PostgreSQL with TypeScript
Think of PostgreSQL as the worldβs most organized librarian π. It knows exactly where every piece of information is stored, can find it lightning-fast, and never loses track of anything. But hereβs the cool part - when you combine it with TypeScript, itβs like having that librarian speak your language perfectly! π£οΈ
Why This Combo Rocks πΈ
PostgreSQL is like a Swiss Army knife π§ for databases:
- ACID compliance - Your data is always consistent
- Advanced features - JSON support, full-text search, and more
- Performance - Handles millions of rows like a champ
- Reliability - Used by companies like Instagram and Spotify
TypeScript adds the magic sauce β¨:
- Type safety - Know your data structure at compile time
- IntelliSense - Your editor becomes super smart
- Refactoring - Change things confidently
- Documentation - Types are living documentation
π§ Basic Setup and Connection
Letβs get our hands dirty! π οΈ First, we need to set up our PostgreSQL playground.
Step 1: Install PostgreSQL π¦
# π macOS with Homebrew
brew install postgresql
brew services start postgresql
# π§ Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib
# πͺ Windows
# Download from postgresql.org
Step 2: Install TypeScript Dependencies π
# π¦ Install the essentials
npm install pg @types/pg
npm install -D @types/node typescript
# π Optional but awesome
npm install dotenv
Step 3: Your First Connection! π
// π database.ts
import { Pool, Client } from 'pg';
// π§ Basic connection (don't do this in production!)
const client = new Client({
user: 'your_username', // π€ Your database user
host: 'localhost', // π Where PostgreSQL lives
database: 'my_awesome_app', // ποΈ Your database name
password: 'your_password', // π Keep this secret!
port: 5432, // πͺ PostgreSQL's favorite port
});
// π Better approach with connection pooling
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432'),
max: 20, // π― Maximum connections
idleTimeoutMillis: 30000, // β° How long to keep idle connections
});
// π Test our connection
const testConnection = async (): Promise<void> => {
try {
const client = await pool.connect();
console.log('π Connected to PostgreSQL!');
const result = await client.query('SELECT NOW()');
console.log('β° Server time:', result.rows[0].now);
client.release(); // π Always return connections to the pool!
} catch (error) {
console.error('π₯ Connection failed:', error);
}
};
export { pool, testConnection };
Step 4: Environment Variables π
// π .env
DB_USER=postgres
DB_HOST=localhost
DB_NAME=my_awesome_app
DB_PASSWORD=your_super_secret_password
DB_PORT=5432
π‘ Practical Examples
Letβs build some real-world applications! ποΈ
Example 1: User Management System π€
First, letβs create our user table:
-- ποΈ Create users table
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- π’ Auto-incrementing ID
username VARCHAR(50) UNIQUE NOT NULL, -- π€ Unique username
email VARCHAR(255) UNIQUE NOT NULL, -- π§ Email address
password_hash VARCHAR(255) NOT NULL, -- π Hashed password
created_at TIMESTAMP DEFAULT NOW(), -- β° When they joined
updated_at TIMESTAMP DEFAULT NOW() -- π Last update
);
Now letβs create type-safe TypeScript code:
// π types/user.ts
export interface User {
id: number; // π’ User ID
username: string; // π€ Username
email: string; // π§ Email
password_hash: string; // π Password hash
created_at: Date; // β° Creation time
updated_at: Date; // π Update time
}
export interface CreateUserData {
username: string; // π€ New username
email: string; // π§ New email
password_hash: string; // π Hashed password
}
// π services/userService.ts
import { pool } from '../database';
import { User, CreateUserData } from '../types/user';
export class UserService {
// π Create a new user
static async createUser(userData: CreateUserData): Promise<User> {
const query = `
INSERT INTO users (username, email, password_hash)
VALUES ($1, $2, $3)
RETURNING *
`;
try {
const result = await pool.query(query, [
userData.username,
userData.email,
userData.password_hash
]);
console.log('β
User created successfully!');
return result.rows[0] as User;
} catch (error) {
console.error('π₯ Failed to create user:', error);
throw new Error('User creation failed');
}
}
// π Find user by email
static async findUserByEmail(email: string): Promise<User | null> {
const query = 'SELECT * FROM users WHERE email = $1';
try {
const result = await pool.query(query, [email]);
if (result.rows.length === 0) {
console.log('π User not found');
return null;
}
console.log('π― User found!');
return result.rows[0] as User;
} catch (error) {
console.error('π₯ Database error:', error);
throw new Error('User lookup failed');
}
}
// π Get all users with pagination
static async getAllUsers(page: number = 1, limit: number = 10): Promise<User[]> {
const offset = (page - 1) * limit;
const query = `
SELECT * FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`;
try {
const result = await pool.query(query, [limit, offset]);
console.log(`π Found ${result.rows.length} users`);
return result.rows as User[];
} catch (error) {
console.error('π₯ Failed to fetch users:', error);
throw new Error('User fetch failed');
}
}
// π Update user
static async updateUser(id: number, updates: Partial<CreateUserData>): Promise<User> {
const setClause = Object.keys(updates)
.map((key, index) => `${key} = $${index + 2}`)
.join(', ');
const query = `
UPDATE users
SET ${setClause}, updated_at = NOW()
WHERE id = $1
RETURNING *
`;
try {
const values = [id, ...Object.values(updates)];
const result = await pool.query(query, values);
if (result.rows.length === 0) {
throw new Error('User not found');
}
console.log('β
User updated successfully!');
return result.rows[0] as User;
} catch (error) {
console.error('π₯ Failed to update user:', error);
throw error;
}
}
}
Example 2: Blog System with Relationships π
Letβs create a more complex example with related tables:
-- π Posts table
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- π¬ Comments table
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
author_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW()
);
TypeScript models with relationships:
// π types/blog.ts
export interface Post {
id: number;
title: string;
content: string;
author_id: number;
published: boolean;
created_at: Date;
updated_at: Date;
// π Optional joined data
author?: User;
comments?: Comment[];
}
export interface Comment {
id: number;
content: string;
post_id: number;
author_id: number;
created_at: Date;
// π Optional joined data
author?: User;
}
// π services/blogService.ts
export class BlogService {
// π Create a new post
static async createPost(authorId: number, title: string, content: string): Promise<Post> {
const query = `
INSERT INTO posts (title, content, author_id)
VALUES ($1, $2, $3)
RETURNING *
`;
try {
const result = await pool.query(query, [title, content, authorId]);
console.log('π Post created!');
return result.rows[0] as Post;
} catch (error) {
console.error('π₯ Failed to create post:', error);
throw new Error('Post creation failed');
}
}
// π Get posts with author information (JOIN!)
static async getPostsWithAuthor(): Promise<Post[]> {
const query = `
SELECT
p.*,
u.username as author_username,
u.email as author_email
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.published = true
ORDER BY p.created_at DESC
`;
try {
const result = await pool.query(query);
// π¨ Transform the results
const posts: Post[] = result.rows.map(row => ({
id: row.id,
title: row.title,
content: row.content,
author_id: row.author_id,
published: row.published,
created_at: row.created_at,
updated_at: row.updated_at,
author: {
id: row.author_id,
username: row.author_username,
email: row.author_email,
} as User
}));
console.log(`π Found ${posts.length} posts with authors`);
return posts;
} catch (error) {
console.error('π₯ Failed to fetch posts:', error);
throw new Error('Posts fetch failed');
}
}
}
π Advanced Concepts
Ready to level up? Letβs explore some advanced PostgreSQL + TypeScript patterns! πͺ
Connection Pooling Best Practices πββοΈ
// π database/pool.ts
import { Pool, PoolConfig } from 'pg';
// π― Advanced pool configuration
const poolConfig: PoolConfig = {
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432'),
// πββοΈ Pool settings
max: 20, // Maximum number of clients
min: 5, // Minimum number of clients
idleTimeoutMillis: 30000, // How long a client can be idle
connectionTimeoutMillis: 2000, // How long to wait for connection
// π§ Advanced settings
statement_timeout: 30000, // Query timeout
query_timeout: 30000, // Another query timeout
application_name: 'my_awesome_app', // Shows up in PostgreSQL logs
};
export const pool = new Pool(poolConfig);
// π Pool event listeners
pool.on('connect', (client) => {
console.log('π New client connected');
});
pool.on('acquire', (client) => {
console.log('π― Client acquired from pool');
});
pool.on('error', (err, client) => {
console.error('π₯ Pool error:', err);
});
// π§Ή Graceful shutdown
process.on('SIGINT', async () => {
console.log('π Shutting down gracefully...');
await pool.end();
process.exit(0);
});
Database Transactions π³
// π services/transactionService.ts
import { PoolClient } from 'pg';
import { pool } from '../database/pool';
export class TransactionService {
// π³ Transfer money between accounts (atomic operation!)
static async transferMoney(
fromUserId: number,
toUserId: number,
amount: number
): Promise<void> {
const client: PoolClient = await pool.connect();
try {
// π¦ Start transaction
await client.query('BEGIN');
// π° Check if sender has enough money
const balanceResult = await client.query(
'SELECT balance FROM accounts WHERE user_id = $1',
[fromUserId]
);
if (balanceResult.rows[0].balance < amount) {
throw new Error('πΈ Insufficient funds!');
}
// π Deduct from sender
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE user_id = $2',
[amount, fromUserId]
);
// π Add to receiver
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE user_id = $2',
[amount, toUserId]
);
// π Log the transaction
await client.query(
`INSERT INTO transactions (from_user_id, to_user_id, amount)
VALUES ($1, $2, $3)`,
[fromUserId, toUserId, amount]
);
// β
Commit transaction
await client.query('COMMIT');
console.log('π Money transfer completed!');
} catch (error) {
// π Rollback on error
await client.query('ROLLBACK');
console.error('π₯ Transaction failed:', error);
throw error;
} finally {
// π Always release the client
client.release();
}
}
}
Query Builder Pattern ποΈ
// π utils/queryBuilder.ts
export class QueryBuilder {
private query: string = '';
private values: any[] = [];
private paramCount: number = 0;
// π― SELECT builder
select(columns: string[]): QueryBuilder {
this.query += `SELECT ${columns.join(', ')} `;
return this;
}
// π FROM builder
from(table: string): QueryBuilder {
this.query += `FROM ${table} `;
return this;
}
// π JOIN builder
join(table: string, condition: string): QueryBuilder {
this.query += `JOIN ${table} ON ${condition} `;
return this;
}
// π― WHERE builder
where(column: string, value: any): QueryBuilder {
this.paramCount++;
const operator = this.query.includes('WHERE') ? 'AND' : 'WHERE';
this.query += `${operator} ${column} = $${this.paramCount} `;
this.values.push(value);
return this;
}
// π ORDER BY builder
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): QueryBuilder {
this.query += `ORDER BY ${column} ${direction} `;
return this;
}
// π’ LIMIT builder
limit(count: number): QueryBuilder {
this.paramCount++;
this.query += `LIMIT $${this.paramCount} `;
this.values.push(count);
return this;
}
// ποΈ Build the final query
build(): { query: string; values: any[] } {
return {
query: this.query.trim(),
values: this.values
};
}
}
// π Usage example
const getUserPosts = async (userId: number, limit: number = 10): Promise<Post[]> => {
const { query, values } = new QueryBuilder()
.select(['p.*', 'u.username'])
.from('posts p')
.join('users u', 'p.author_id = u.id')
.where('p.author_id', userId)
.where('p.published', true)
.orderBy('p.created_at', 'DESC')
.limit(limit)
.build();
console.log('π Generated query:', query);
const result = await pool.query(query, values);
return result.rows as Post[];
};
β οΈ Common Pitfalls and Solutions
Letβs learn from common mistakes! π
Pitfall 1: SQL Injection π¨
β NEVER do this:
// π DANGEROUS! SQL injection vulnerability
const getUser = async (email: string) => {
const query = `SELECT * FROM users WHERE email = '${email}'`;
return await pool.query(query);
};
// π Hacker input: "'; DROP TABLE users; --"
// π₯ Your entire users table gets deleted!
β Always use parameterized queries:
// π‘οΈ SAFE! Parameterized query
const getUser = async (email: string) => {
const query = 'SELECT * FROM users WHERE email = $1';
return await pool.query(query, [email]);
};
// π PostgreSQL handles escaping automatically!
Pitfall 2: Connection Leaks π§
β Donβt forget to release connections:
// π§ Connection leak - bad!
const badExample = async () => {
const client = await pool.connect();
const result = await client.query('SELECT * FROM users');
return result.rows;
// π₯ Connection never released!
};
β Always release or use pool directly:
// β
Good - using pool directly
const goodExample = async () => {
const result = await pool.query('SELECT * FROM users');
return result.rows;
};
// β
Also good - explicit release
const alsoGoodExample = async () => {
const client = await pool.connect();
try {
const result = await client.query('SELECT * FROM users');
return result.rows;
} finally {
client.release(); // π Always release!
}
};
Pitfall 3: Ignoring Errors π
β Silent failures are the worst:
// π Ignoring errors - terrible!
const badErrorHandling = async (email: string) => {
try {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
return result.rows[0];
} catch (error) {
// π± Silent failure!
return null;
}
};
β Proper error handling:
// π― Good error handling
const goodErrorHandling = async (email: string): Promise<User | null> => {
try {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (result.rows.length === 0) {
return null; // π User not found, that's okay
}
return result.rows[0] as User;
} catch (error) {
console.error('π₯ Database error in getUserByEmail:', error);
// π Log important details for debugging
console.error('π§ Email:', email);
console.error('β° Timestamp:', new Date().toISOString());
// π¨ Re-throw so caller can handle appropriately
throw new Error(`Failed to fetch user: ${error.message}`);
}
};
π οΈ Best Practices
Here are the golden rules for PostgreSQL + TypeScript success! π
1. Database Design Principles π¨
-- π― Good table design
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- π Use UUIDs for distributed systems
user_id INTEGER NOT NULL REFERENCES users(id), -- π Foreign key constraints
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- π Meaningful defaults
total_amount DECIMAL(10,2) NOT NULL, -- π° Proper decimal for money
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- β° Always use timezone
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- π Constraints make your data trustworthy
CONSTRAINT valid_status CHECK (status IN ('pending', 'processing', 'completed', 'cancelled')),
CONSTRAINT positive_amount CHECK (total_amount > 0)
);
-- π Indexes for performance
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
2. Type-Safe Database Layer π‘οΈ
// π types/database.ts
// π― Strict database types
export interface DatabaseUser {
readonly id: number; // π ID should never change
username: string;
email: string;
password_hash: string;
readonly created_at: Date; // π Creation time shouldn't change
updated_at: Date;
}
// π¨ Separate types for different operations
export type CreateUserInput = Omit<DatabaseUser, 'id' | 'created_at' | 'updated_at'>;
export type UpdateUserInput = Partial<Pick<DatabaseUser, 'username' | 'email'>>;
export type UserResponse = Omit<DatabaseUser, 'password_hash'>; // π Never return password
// π database/userRepository.ts
export class UserRepository {
// π― Type-safe repository pattern
static async create(userData: CreateUserInput): Promise<UserResponse> {
const query = `
INSERT INTO users (username, email, password_hash)
VALUES ($1, $2, $3)
RETURNING id, username, email, created_at, updated_at
`;
const result = await pool.query(query, [
userData.username,
userData.email,
userData.password_hash
]);
return result.rows[0] as UserResponse;
}
// π Type-safe queries with validation
static async findById(id: number): Promise<UserResponse | null> {
if (!Number.isInteger(id) || id <= 0) {
throw new Error('π¨ Invalid user ID');
}
const query = `
SELECT id, username, email, created_at, updated_at
FROM users
WHERE id = $1
`;
const result = await pool.query(query, [id]);
return result.rows[0] as UserResponse || null;
}
}
3. Performance Optimization π
// π database/performance.ts
export class PerformanceOptimizer {
// π Batch operations for better performance
static async createMultipleUsers(users: CreateUserInput[]): Promise<UserResponse[]> {
if (users.length === 0) return [];
// π― Single query instead of multiple queries
const values: string[] = [];
const params: any[] = [];
users.forEach((user, index) => {
const offset = index * 3;
values.push(`($${offset + 1}, $${offset + 2}, $${offset + 3})`);
params.push(user.username, user.email, user.password_hash);
});
const query = `
INSERT INTO users (username, email, password_hash)
VALUES ${values.join(', ')}
RETURNING id, username, email, created_at, updated_at
`;
const result = await pool.query(query, params);
console.log(`π Created ${result.rows.length} users in one query!`);
return result.rows as UserResponse[];
}
// π Efficient pagination
static async getUsersPaginated(
page: number,
pageSize: number = 20
): Promise<{ users: UserResponse[]; total: number; hasMore: boolean }> {
const offset = (page - 1) * pageSize;
// π― Get data and count in parallel
const [dataResult, countResult] = await Promise.all([
pool.query(`
SELECT id, username, email, created_at, updated_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`, [pageSize + 1, offset]), // π― Get one extra to check if there's more
pool.query('SELECT COUNT(*) FROM users')
]);
const users = dataResult.rows.slice(0, pageSize) as UserResponse[];
const hasMore = dataResult.rows.length > pageSize;
const total = parseInt(countResult.rows[0].count);
return { users, total, hasMore };
}
}
4. Environment Configuration π
// π config/database.ts
import { config } from 'dotenv';
import { PoolConfig } from 'pg';
config(); // π§ Load environment variables
// π― Validate required environment variables
const requiredEnvVars = ['DB_USER', 'DB_HOST', 'DB_NAME', 'DB_PASSWORD'];
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
if (missingVars.length > 0) {
throw new Error(`π¨ Missing required environment variables: ${missingVars.join(', ')}`);
}
// ποΈ Environment-specific configurations
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
export const databaseConfig: PoolConfig = {
user: process.env.DB_USER!,
host: process.env.DB_HOST!,
database: process.env.DB_NAME!,
password: process.env.DB_PASSWORD!,
port: parseInt(process.env.DB_PORT || '5432'),
// π― Environment-specific settings
max: isProduction ? 20 : 5,
min: isProduction ? 5 : 1,
idleTimeoutMillis: isProduction ? 30000 : 10000,
// π§ SSL settings for production
ssl: isProduction ? { rejectUnauthorized: false } : false,
// π Logging
log: isDevelopment ? console.log : undefined,
};
// π Export ready-to-use pool
export const pool = new Pool(databaseConfig);
π§ͺ Hands-On Exercise
Time to put your skills to the test! πͺ Letβs build a complete e-commerce system with products, categories, and orders.
Your Mission π―
Create a type-safe e-commerce database system with:
- π¦ Products with categories
- π Shopping cart functionality
- π Order management
- π€ Customer accounts
Database Schema ποΈ
-- π·οΈ Categories table
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- π¦ Products table
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
stock_quantity INTEGER NOT NULL DEFAULT 0,
category_id INTEGER REFERENCES categories(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT positive_price CHECK (price > 0),
CONSTRAINT non_negative_stock CHECK (stock_quantity >= 0)
);
-- π Shopping cart items
CREATE TABLE cart_items (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(id) ON DELETE CASCADE,
quantity INTEGER NOT NULL DEFAULT 1,
added_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT positive_quantity CHECK (quantity > 0),
UNIQUE(user_id, product_id)
);
-- π Orders
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER REFERENCES users(id),
status VARCHAR(20) DEFAULT 'pending',
total_amount DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT valid_order_status CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled'))
);
Your Challenge π
Try to implement these TypeScript services:
- ProductService - CRUD operations for products
- CartService - Add/remove items, calculate totals
- OrderService - Create orders from cart, update status
π Click to see the solution!
// π types/ecommerce.ts
export interface Category {
id: number;
name: string;
description?: string;
created_at: Date;
}
export interface Product {
id: number;
name: string;
description?: string;
price: number;
stock_quantity: number;
category_id: number;
created_at: Date;
updated_at: Date;
category?: Category;
}
export interface CartItem {
id: number;
user_id: number;
product_id: number;
quantity: number;
added_at: Date;
product?: Product;
}
export interface Order {
id: string;
user_id: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
total_amount: number;
created_at: Date;
}
// π services/productService.ts
export class ProductService {
// π¦ Get products with categories
static async getProductsWithCategories(): Promise<Product[]> {
const query = `
SELECT
p.*,
c.name as category_name,
c.description as category_description
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
ORDER BY p.created_at DESC
`;
const result = await pool.query(query);
return result.rows.map(row => ({
id: row.id,
name: row.name,
description: row.description,
price: parseFloat(row.price),
stock_quantity: row.stock_quantity,
category_id: row.category_id,
created_at: row.created_at,
updated_at: row.updated_at,
category: row.category_name ? {
id: row.category_id,
name: row.category_name,
description: row.category_description,
created_at: new Date()
} : undefined
} as Product));
}
// π Search products
static async searchProducts(searchTerm: string): Promise<Product[]> {
const query = `
SELECT * FROM products
WHERE name ILIKE $1 OR description ILIKE $1
ORDER BY name
`;
const result = await pool.query(query, [`%${searchTerm}%`]);
return result.rows as Product[];
}
}
// π services/cartService.ts
export class CartService {
// π Add item to cart
static async addToCart(userId: number, productId: number, quantity: number = 1): Promise<CartItem> {
const query = `
INSERT INTO cart_items (user_id, product_id, quantity)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, product_id)
DO UPDATE SET quantity = cart_items.quantity + $3
RETURNING *
`;
const result = await pool.query(query, [userId, productId, quantity]);
return result.rows[0] as CartItem;
}
// π Get cart with product details
static async getCartWithProducts(userId: number): Promise<CartItem[]> {
const query = `
SELECT
ci.*,
p.name as product_name,
p.price as product_price,
p.stock_quantity
FROM cart_items ci
JOIN products p ON ci.product_id = p.id
WHERE ci.user_id = $1
ORDER BY ci.added_at DESC
`;
const result = await pool.query(query, [userId]);
return result.rows.map(row => ({
id: row.id,
user_id: row.user_id,
product_id: row.product_id,
quantity: row.quantity,
added_at: row.added_at,
product: {
id: row.product_id,
name: row.product_name,
price: parseFloat(row.product_price),
stock_quantity: row.stock_quantity
} as Product
} as CartItem));
}
// π° Calculate cart total
static async getCartTotal(userId: number): Promise<number> {
const query = `
SELECT SUM(ci.quantity * p.price) as total
FROM cart_items ci
JOIN products p ON ci.product_id = p.id
WHERE ci.user_id = $1
`;
const result = await pool.query(query, [userId]);
return parseFloat(result.rows[0].total || '0');
}
}
// π services/orderService.ts
export class OrderService {
// π Create order from cart
static async createOrderFromCart(userId: number): Promise<Order> {
const client = await pool.connect();
try {
await client.query('BEGIN');
// π° Calculate total
const totalResult = await client.query(`
SELECT SUM(ci.quantity * p.price) as total
FROM cart_items ci
JOIN products p ON ci.product_id = p.id
WHERE ci.user_id = $1
`, [userId]);
const total = parseFloat(totalResult.rows[0].total || '0');
if (total === 0) {
throw new Error('π Cart is empty!');
}
// π Create order
const orderResult = await client.query(`
INSERT INTO orders (user_id, total_amount)
VALUES ($1, $2)
RETURNING *
`, [userId, total]);
// π§Ή Clear cart
await client.query('DELETE FROM cart_items WHERE user_id = $1', [userId]);
await client.query('COMMIT');
console.log('π Order created successfully!');
return orderResult.rows[0] as Order;
} catch (error) {
await client.query('ROLLBACK');
console.error('π₯ Order creation failed:', error);
throw error;
} finally {
client.release();
}
}
}
π Key Takeaways
Congratulations! π Youβve mastered PostgreSQL with TypeScript! Hereβs what you now know:
β
Database Connections - Pool management and best practices
β
Type Safety - Proper TypeScript types for database operations
β
SQL Operations - CRUD, joins, transactions, and complex queries
β
Performance - Connection pooling, batching, and optimization
β
Security - Parameterized queries and proper error handling
β
Real-World Patterns - Repository pattern, query builders, and more
Youβre now equipped to build scalable, type-safe database applications! πͺ
π€ Next Steps
Ready to keep growing? Hereβs your learning path:
- π Advanced PostgreSQL - Learn about indexing, partitioning, and performance tuning
- ποΈ Database Migrations - Version control your database schema
- π ORM Libraries - Explore Prisma, TypeORM, or Drizzle for even more type safety
- π Production Deployment - Learn about database hosting, backups, and monitoring
- π Real-time Features - Implement subscriptions and live updates
Keep coding, keep learning, and most importantly, have fun building amazing applications! πβ¨
Youβve got this! πͺπ