Lesson 8 of 8
6 min read
Advanced Node.js

Database Integration

Connect to MongoDB, PostgreSQL, and use ORMs like Prisma

Choosing a Database

Node.js works well with both SQL and NoSQL databases. Your choice depends on your data structure, scalability needs, and query patterns.

📊 Database Comparison

Type Examples Best For
SQLPostgreSQL, MySQLComplex queries, transactions, relations
NoSQLMongoDB, RedisFlexible schema, high write volume
GraphNeo4jHighly connected data

MongoDB with Mongoose

const mongoose = require('mongoose');

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000,
});

mongoose.connection.on('connected', () => {
  console.log('Connected to MongoDB');
});

mongoose.connection.on('error', (err) => {
  console.error('MongoDB error:', err);
});

// Define a schema
const userSchema = new mongoose.Schema({
  email: { 
    type: String, 
    required: true, 
    unique: true,
    lowercase: true,
    trim: true
  },
  password: { type: String, required: true, select: false },
  name: { type: String, required: true },
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now }
});

// Add methods
userSchema.methods.toJSON = function() {
  const obj = this.toObject();
  delete obj.password;
  return obj;
};

// Add statics
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email: email.toLowerCase() });
};

// Create model
const User = mongoose.model('User', userSchema);

// CRUD operations
async function examples() {
  // Create
  const user = await User.create({
    email: 'test@example.com',
    password: hashedPassword,
    name: 'Test User'
  });

  // Read
  const users = await User.find({ role: 'user' })
    .select('name email')
    .sort({ createdAt: -1 })
    .limit(10);

  // Update
  await User.findByIdAndUpdate(id, { name: 'New Name' }, { new: true });

  // Delete
  await User.findByIdAndDelete(id);
}

PostgreSQL with pg

const { Pool } = require('pg');

// Create a connection pool
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,                    // Max connections
  idleTimeoutMillis: 30000,   // Close idle connections
  connectionTimeoutMillis: 2000,
});

pool.on('error', (err) => {
  console.error('Unexpected pool error:', err);
});

// Query helper
async function query(text, params) {
  const start = Date.now();
  const result = await pool.query(text, params);
  const duration = Date.now() - start;
  console.log('Query executed:', { text, duration, rows: result.rowCount });
  return result;
}

// CRUD operations
async function examples() {
  // Create table
  await query(`
    CREATE TABLE IF NOT EXISTS users (
      id SERIAL PRIMARY KEY,
      email VARCHAR(255) UNIQUE NOT NULL,
      name VARCHAR(255) NOT NULL,
      created_at TIMESTAMP DEFAULT NOW()
    )
  `);

  // Insert (parameterized - prevents SQL injection!)
  const { rows } = await query(
    'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
    ['test@example.com', 'Test User']
  );

  // Select
  const users = await query(
    'SELECT * FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2',
    [new Date('2024-01-01'), 10]
  );

  // Update
  await query(
    'UPDATE users SET name = $1 WHERE id = $2',
    ['New Name', 1]
  );

  // Delete
  await query('DELETE FROM users WHERE id = $1', [1]);
}

// Transaction example
async function transferMoney(fromId, toId, amount) {
  const client = await pool.connect();
  
  try {
    await client.query('BEGIN');
    
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
      [amount, fromId]
    );
    
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount, toId]
    );
    
    await client.query('COMMIT');
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

Prisma ORM (Modern Choice)

# Install Prisma
npm install prisma @prisma/client
npx prisma init

# schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

# Generate client after schema changes
npx prisma generate

# Run migrations
npx prisma migrate dev --name init
// Using Prisma Client
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

async function main() {
  // Create user with related posts
  const user = await prisma.user.create({
    data: {
      email: 'alice@example.com',
      name: 'Alice',
      posts: {
        create: [
          { title: 'First Post', content: 'Hello World' },
          { title: 'Second Post', content: 'Prisma is great!' }
        ]
      }
    },
    include: { posts: true }
  });

  // Find with relations
  const users = await prisma.user.findMany({
    where: {
      email: { contains: '@example.com' }
    },
    include: {
      posts: {
        where: { published: true },
        orderBy: { createdAt: 'desc' }
      }
    }
  });

  // Update
  const updated = await prisma.user.update({
    where: { id: 1 },
    data: { name: 'Alice Updated' }
  });

  // Delete
  await prisma.user.delete({ where: { id: 1 } });

  // Transaction
  const [post, user] = await prisma.$transaction([
    prisma.post.create({ data: { title: 'New', authorId: 1 } }),
    prisma.user.update({ where: { id: 1 }, data: { name: 'Updated' } })
  ]);
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());

Redis for Caching

const { createClient } = require('redis');

const redis = createClient({ url: process.env.REDIS_URL });

redis.on('error', (err) => console.error('Redis error:', err));
redis.on('connect', () => console.log('Connected to Redis'));

await redis.connect();

// Basic operations
await redis.set('key', 'value');
await redis.set('key', 'value', { EX: 3600 });  // Expires in 1 hour

const value = await redis.get('key');

// Cache-aside pattern
async function getCachedUser(id) {
  const cacheKey = `user:${id}`;
  
  // Check cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // Fetch from database
  const user = await prisma.user.findUnique({ where: { id } });
  
  // Store in cache
  if (user) {
    await redis.set(cacheKey, JSON.stringify(user), { EX: 300 });
  }
  
  return user;
}

// Invalidate cache on update
async function updateUser(id, data) {
  const user = await prisma.user.update({ where: { id }, data });
  await redis.del(`user:${id}`);  // Invalidate cache
  return user;
}

// Rate limiting with Redis
async function checkRateLimit(ip) {
  const key = `ratelimit:${ip}`;
  const requests = await redis.incr(key);
  
  if (requests === 1) {
    await redis.expire(key, 60);  // 1 minute window
  }
  
  return requests <= 100;  // 100 requests per minute
}

Connection Pooling Best Practices

// PostgreSQL pool configuration
const pool = new Pool({
  max: 20,                        // Max connections
  min: 5,                         // Min connections to keep
  idleTimeoutMillis: 30000,       // Close idle after 30s
  connectionTimeoutMillis: 5000,  // Connection timeout
  
  // For serverless (Vercel, Lambda)
  maxUses: 7500,  // Close connection after N uses
});

// Mongoose connection options
mongoose.connect(uri, {
  maxPoolSize: 10,
  minPoolSize: 5,
  maxIdleTimeMS: 30000,
  serverSelectionTimeoutMS: 5000,
});

// Prisma with connection limit
// In schema.prisma or via URL
// DATABASE_URL="postgresql://...?connection_limit=10"

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('Shutting down...');
  
  await pool.end();           // PostgreSQL
  await mongoose.disconnect(); // MongoDB
  await prisma.$disconnect();  // Prisma
  await redis.quit();          // Redis

  
  
  process.exit(0);
});

💡 Best Practices

  • • Always use connection pooling in production
  • • Use parameterized queries to prevent SQL injection
  • • Implement proper error handling and retries
  • • Add indexes for frequently queried fields
  • • Use Redis or similar for caching hot data
  • • Close connections gracefully on shutdown

Continue Learning