1. Introduction đ
Almost every real-world Node.js application needs to persist data somewhere. This tutorial covers database fundamentals, the most popular databases and drivers in the Node.js ecosystem, and the ORMs that make working with them safer and more productive â from raw connections all the way to migrations and query optimization.
Information
2. Database Fundamentals đ§ą
Databases fall broadly into a few categories, each optimized for different data shapes and access patterns.
- Relational (SQL) â structured tables with fixed schemas and strong relationships.
- Document (NoSQL) â flexible, JSON-like documents with dynamic schemas.
- Key-value â extremely fast lookups by key, often used for caching (e.g. Redis).
- Embedded â lightweight, file-based databases requiring no separate server (e.g. SQLite).
Tip
3. SQL vs NoSQL âī¸
| Aspect | SQL | NoSQL |
|---|---|---|
| Schema | Fixed, enforced | Flexible, dynamic |
| Relationships | Strongvia joins & foreign keys | Often denormalized |
| Consistency | ACID by default | Varies (often eventual) |
| Scaling | Primarily vertical | Often horizontal |
| Examples | PostgreSQL, MySQL | MongoDB, Redis |
Best Practice
4. Database Drivers đ
A database driver is the low-level library that handles the actual network protocol and communication with the database server.
Terminal
npm install pg # PostgreSQL
npm install mysql2 # MySQL
npm install mongodb # MongoDB
npm install ioredis # RedisNote
5. Connection Management đ
Establishing a database connection is relatively expensive, so connections should be created once and reused across requests.
src/db/client.ts
import { Client } from "pg";
const client = new Client({ connectionString: process.env.DATABASE_URL });
async function connect(): Promise<void> {
await client.connect();
console.log("Connected to PostgreSQL â
");
}Warning
6. Connection Pooling đ
A connection pool maintains a set of reusable, open connections, avoiding the overhead of establishing a new one per request.
src/db/pool.ts
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30_000,
});
export async function query<T>(text: string, params: unknown[] = []): Promise<T[]> {
const { rows } = await pool.query(text, params);
return rows;
}Tip
7. MongoDB đ
MongoDB is a document-oriented NoSQL database storing data as flexible, JSON-like BSON documents.
src/db/mongo.ts
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.MONGO_URL as string);
await client.connect();
const db = client.db("myapp");
const users = db.collection("users");
const user = await users.findOne({ email: "jane@example.com" });
await users.insertOne({ name: "Jane", email: "jane@example.com" });Information
8. PostgreSQL đ
PostgreSQL is a powerful, open-source relational database known for standards compliance, extensibility, and reliability.
src/db/postgres.ts
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query(
"SELECT id, name FROM users WHERE active = $1",
[true]
);Tip
9. MySQL đŦ
MySQL is another widely used open-source relational database, popular for its simplicity and broad hosting support.
src/db/mysql.ts
import mysql from "mysql2/promise";
const pool = mysql.createPool({
host: "localhost",
user: "root",
database: "myapp",
connectionLimit: 10,
});
const [rows] = await pool.query("SELECT * FROM users WHERE id = ?", [1]);Note
10. SQLite đĒļ
SQLite is a lightweight, file-based, embedded database â ideal for local development, testing, or small applications with no separate server.
src/db/sqlite.ts
import Database from "better-sqlite3";
const db = new Database("app.db");
db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
const insert = db.prepare("INSERT INTO users (name) VALUES (?)");
insert.run("Jane");Tip
11. Redis đ´
Redis is an in-memory key-value store, commonly used for caching, session storage, pub/sub messaging, and rate limiting.
src/db/redis.ts
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL as string);
await redis.set("session:abc123", JSON.stringify({ userId: 1 }), "EX", 3600);
const session = await redis.get("session:abc123");Important
12. Prisma ORM đ
Prisma generates a fully typed client from a declarative schema file, giving excellent autocomplete and compile-time safety.
prisma/schema.prisma
model User {
id String @id @default(uuid())
name String
email String @unique
}src/db/prisma-client.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const user = await prisma.user.create({
data: { name: "Jane", email: "jane@example.com" },
});Best Practice
13. Drizzle ORM đ
Drizzle is a lightweight, SQL-first ORM that stays close to raw SQL while providing full TypeScript type inference.
src/db/schema.ts
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
email: varchar("email", { length: 255 }).unique().notNull(),
});src/db/queries.ts
import { db } from "./client.js";
import { users } from "./schema.js";
import { eq } from "drizzle-orm";
const user = await db.select().from(users).where(eq(users.email, "jane@example.com"));Tip
14. Mongoose đĻĢ
Mongoose is the most popular ODM (Object-Document Mapper) for MongoDB, providing schemas, validation, and middleware hooks.
src/models/User.ts
import { Schema, model } from "mongoose";
interface IUser {
name: string;
email: string;
}
const userSchema = new Schema<IUser>({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
});
export const User = model<IUser>("User", userSchema);src/index.ts
const user = await User.create({ name: "Jane", email: "jane@example.com" });
const found = await User.findOne({ email: "jane@example.com" });15. Sequelize đ
Sequelize is a mature, promise-based ORM supporting PostgreSQL, MySQL, SQLite, and more, with a rich feature set for relational data.
src/models/User.ts
import { DataTypes, Model } from "sequelize";
import { sequelize } from "../db/connection.js";
class User extends Model {
declare id: number;
declare name: string;
declare email: string;
}
User.init(
{
name: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, unique: true, allowNull: false },
},
{ sequelize, modelName: "User" }
);16. TypeORM đ
TypeORM uses decorators to define entities, closely resembling patterns familiar to developers coming from Java or C# backgrounds.
src/entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column()
name!: string;
@Column({ unique: true })
email!: string;
}Note
17. CRUD Operations đ
Create, Read, Update, and Delete form the foundation of nearly all database interactions.
CRUD with Prisma
await prisma.user.create({ data: { name: "Jane", email: "jane@x.com" } });
await prisma.user.findMany();
await prisma.user.update({ where: { id }, data: { name: "Janet" } });
await prisma.user.delete({ where: { id } });CRUD with Mongoose
await User.create({ name: "Jane", email: "jane@x.com" });
await User.find();
await User.updateOne({ _id: id }, { name: "Janet" });
await User.deleteOne({ _id: id });18. Transactions đ
Transactions group multiple operations into a single atomic unit â either all succeed, or none take effect.
src/db/transaction.ts
await prisma.$transaction(async (tx) => {
await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: 100 } } });
await tx.account.update({ where: { id: toId }, data: { balance: { increment: 100 } } });
});Important
19. Migrations đ
Migrations track and apply incremental changes to your database schema in a version-controlled, repeatable way.
Terminal
npx prisma migrate dev --name add_users_table
npx prisma migrate deploy # productionBest Practice
20. Seeding đą
Seed scripts populate a database with initial or sample data, useful for local development and testing environments.
prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.user.createMany({
data: [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
],
skipDuplicates: true,
});
}
main().finally(() => prisma.$disconnect());21. Query Optimization đ
Poorly written queries are a leading cause of slow applications. A few habits go a long way toward keeping queries fast.
- Select only the columns you need instead of SELECT *.
- Avoid N+1 queries by batching or using JOINs / include.
- Use EXPLAIN ANALYZE to inspect a query's execution plan.
- Paginate large result sets instead of fetching everything at once.
src/db/n-plus-one-fix.ts
// â N+1 â one query per user
for (const user of users) {
user.orders = await getOrdersByUserId(user.id);
}
// â
Single batched query with Prisma's include
const usersWithOrders = await prisma.user.findMany({ include: { orders: true } });22. Indexing đ
Indexes dramatically speed up lookups on frequently queried columns, at the cost of slightly slower writes.
Create an index
CREATE INDEX idx_users_email ON users(email);prisma/schema.prisma
model User {
id String @id @default(uuid())
email String @unique
@@index([email])
}Tip
23. Data Validation â
Database constraints alone aren't enough â validating data at the application layer catches errors earlier with clearer messages.
src/validation/user.ts
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
function createUser(input: unknown) {
const data = userSchema.parse(input);
return prisma.user.create({ data });
}Best Practice
24. Error Handling â ī¸
Database errors â constraint violations, connection failures, timeouts â must be caught and handled gracefully.
src/db/errors.ts
import { Prisma } from "@prisma/client";
try {
await prisma.user.create({ data: { name: "Jane", email: "jane@example.com" } });
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
throw new Error("Email already in use");
}
throw error;
}Caution
25. Performance Optimization âĄ
- Use connection pooling in all production environments.
- Cache frequently-read, rarely-changed data with Redis or an in-memory cache.
- Batch writes where possible instead of issuing many individual INSERT statements.
- Monitor slow queries and add indexes based on real query patterns, not guesses.
Reference
26. Best Practices â
- Always use parameterized queries or an ORM to prevent SQL injection.
- Wrap multi-step writes in transactions to preserve consistency.
- Keep schema changes in version-controlled migrations.
- Validate data at the application layer before it reaches the database.
- Index columns based on actual, observed query patterns.
27. Common Mistakes â ī¸
| Mistake | Consequence |
|---|---|
| Creating a new connection per request | Connection exhaustion under load |
| Fetching related data in a loop (N+1) | Severe performance degradation |
| Skipping migrations, editing the DB manually | Schema drift across environments |
| No indexes on frequently filtered columns | Slow queries as data grows |
| Trusting client input without validation | Data corruption or injection vulnerabilities |
28. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
Question
Answer
29. Summary đ
Database integration in Node.js spans choosing the right database for your data, managing connections efficiently, and using ORMs like Prisma, Drizzle, Mongoose, Sequelize, or TypeORM to write safer, more maintainable queries.
- Choose SQL or NoSQL based on your data shape and relationships.
- Use connection pooling in every production deployment.
- Wrap multi-step writes in transactions for consistency.
- Track schema changes with migrations, and seed data for development.
- Optimize with indexes and batched queries as your data grows.