Store and retrieve data persistently with MySQL or PostgreSQL. Perfect for user data, content, orders, and more.
You need a database when your app needs to:
For very simple apps (static content, simple forms), you might get away with JSON files or SQLite. But for anything with users or growing data, use a proper database.
| Feature | MySQL | PostgreSQL |
|---|---|---|
| Ease of Setup | Slightly easier | More configuration |
| Performance | Great for reads | Better for complex queries |
| JSON Support | Good | Excellent (JSONB) |
| Full-Text Search | Basic | Advanced |
| Community/Hosting | More common | Growing fast |
| Best For | Web apps, WordPress | Complex apps, analytics |
Use MySQL if you're building a typical web app, blog, or e-commerce site. Use PostgreSQL if you need complex queries, JSON storage, or advanced features.
Let's install MySQL and create your first database.
# Install MySQL
sudo apt update
sudo apt install mysql-server -y
# Secure the installation
sudo mysql_secure_installation
# Log in to MySQL
sudo mysql
# Create database and user
CREATE DATABASE myapp;
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'your_secure_password';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
// db.js - MySQL connection with pooling
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Helper function for queries
async function query(sql, params) {
const [rows] = await pool.execute(sql, params);
return rows;
}
module.exports = { pool, query };
PostgreSQL offers more advanced features for complex applications.
# Install PostgreSQL
sudo apt update
sudo apt install postgresql postgresql-contrib -y
# Switch to postgres user
sudo -u postgres psql
# Create database and user
CREATE USER myapp_user WITH PASSWORD 'your_secure_password';
CREATE DATABASE myapp OWNER myapp_user;
GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp_user;
\q
// db.js - PostgreSQL connection with pooling
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Or use individual params:
// host: process.env.DB_HOST,
// user: process.env.DB_USER,
// password: process.env.DB_PASSWORD,
// database: process.env.DB_NAME,
max: 20,
idleTimeoutMillis: 30000,
});
// Helper function for queries
async function query(text, params) {
const result = await pool.query(text, params);
return result.rows;
}
module.exports = { pool, query };
A good database schema is the foundation of your app. Plan your tables before writing code.
-- Users table
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
);
-- Products table
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
stock INT DEFAULT 0,
image_url VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Orders table
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status ENUM('pending','paid','shipped','delivered') DEFAULT 'pending',
total DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Order items table
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
price_at_time DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
CRUD stands for Create, Read, Update, Delete - the four basic operations you'll do with your data.
const bcrypt = require('bcrypt');
const { query } = require('./db');
async function createUser(email, password, name) {
const passwordHash = await bcrypt.hash(password, 10);
try {
const result = await query(
'INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)',
[email, passwordHash, name]
);
return { id: result.insertId, email, name };
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
throw new Error('Email already exists');
}
throw error;
}
}
async function getUserById(id) {
const users = await query(
'SELECT id, email, name, created_at FROM users WHERE id = ?',
[id]
);
return users[0] || null;
}
async function getUserByEmail(email) {
const users = await query(
'SELECT * FROM users WHERE email = ?',
[email]
);
return users[0] || null;
}
async function getProducts(page = 1, limit = 20) {
const offset = (page - 1) * limit;
return await query(
'SELECT * FROM products ORDER BY created_at DESC LIMIT ? OFFSET ?',
[limit, offset]
);
}
async function searchProducts(searchTerm) {
return await query(
'SELECT * FROM products WHERE name LIKE ? OR description LIKE ?',
[`%${searchTerm}%`, `%${searchTerm}%`]
);
}
async function updateUserName(userId, newName) {
const result = await query(
'UPDATE users SET name = ? WHERE id = ?',
[newName, userId]
);
return result.affectedRows > 0;
}
async function deleteUser(userId) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// Delete order items first (foreign key constraint)
await connection.execute(
'DELETE oi FROM order_items oi JOIN orders o ON oi.order_id = o.id WHERE o.user_id = ?',
[userId]
);
// Delete orders
await connection.execute('DELETE FROM orders WHERE user_id = ?', [userId]);
// Delete user
await connection.execute('DELETE FROM users WHERE id = ?', [userId]);
await connection.commit();
return true;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
NEVER concatenate user input directly into SQL queries. Always use parameterized queries (prepared statements) as shown in the examples above.
#!/bin/bash
# /scripts/backup-db.sh
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="myapp"
# Create backup
mysqldump -u myapp_user -p"$DB_PASSWORD" $DB_NAME > $BACKUP_DIR/backup_$TIMESTAMP.sql
# Compress
gzip $BACKUP_DIR/backup_$TIMESTAMP.sql
# Delete backups older than 7 days
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +7 -delete
# Add to crontab:
# 0 3 * * * /scripts/backup-db.sh
Test your backups regularly by restoring to a test database. A backup that doesn't restore is useless!