PART 1: INSTALLATION & SETUP
Step 1: Install Node.js
Go to nodejs.org
Download and install the LTS version (Long Term Support)
Verify installation:
# Open Terminal (Mac/Linux) or Command Prompt/PowerShell (Windows) node --version npm --version
You should see version numbers like
v20.x.xand10.x.x
Step 2: Understanding the Tools
Node.js: Lets you run JavaScript outside the browser (on a server)
npm (Node Package Manager): Installs libraries/packages
Express: A web framework for Node.js (makes building websites/servers easier)
PART 2: YOUR FIRST NODE.JS PROGRAM
Create a project folder:
# Open terminal/command prompt mkdir my-first-project cd my-first-project
Create your first JavaScript file:
# Windows users can use Notepad or VS Code # Create a file named hello.js
Add this code to
hello.js:console.log("Hello, World!");
Run it:
node hello.jsYou should see "Hello, World!" printed in the terminal.
PART 3: CREATING AN EXPRESS SERVER
Step 1: Initialize Your Project
# In your project folder npm init -y
This creates a package.json file (project configuration).
Step 2: Install Express
npm install express
This creates a node_modules folder (where packages are stored).
Step 3: Create Your Server File
Create a file named server.js:
// 1. Import Express const express = require('express'); // 2. Create an Express application const app = express(); // 3. Define the port const PORT = 3000; // 4. Create your first route app.get('/', (request, response) => { response.send('Welcome to my website!'); }); // 5. Start the server app.listen(PORT, () => { console.log(`✅ Server is running at: http://localhost:${PORT}`); });
Step 4: Run Your Server
node server.jsStep 5: Visit Your Website
Open your browser and go to: http://localhost:3000
You should see "Welcome to my website!"
PART 4: UNDERSTANDING THE CODE
Let's break down server.js line by line:
// Line 1: Import the Express library // Think of this like: "I want to use Express tools" const express = require('express'); // Line 2: Create our app (like building a house blueprint) const app = express(); // Line 3: Choose a door number (port) for our server const PORT = 3000; // Line 4-6: Create a route (like a room in the house) // When someone visits the homepage ('/'), send a response app.get('/', (req, res) => { res.send('Welcome to my website!'); }); // Line 8-10: Turn on the server (open the door) app.listen(PORT, () => { console.log(`✅ Server is running at: http://localhost:${PORT}`); });
PART 5: ADDING MORE PAGES/ROUTES
Update your server.js:
const express = require('express'); const app = express(); const PORT = 3000; // Homepage app.get('/', (req, res) => { res.send(` <h1>Welcome to My Website!</h1> <p>Check out these pages:</p> <ul> <li><a href="/about">About Us</a></li> <li><a href="/contact">Contact</a></li> <li><a href="/users/john">John's Profile</a></li> </ul> `); }); // About page app.get('/about', (req, res) => { res.send('<h1>About Us</h1><p>We are a learning company!</p>'); }); // Contact page app.get('/contact', (req, res) => { res.send('<h1>Contact Page</h1><p>Email us at: hello@example.com</p>'); }); // Dynamic route (can handle any username) app.get('/users/:username', (req, res) => { res.send(`<h1>Profile: ${req.params.username}</h1>`); }); // JSON response (for APIs) app.get('/api/data', (req, res) => { res.json({ message: "Hello from API!", users: ["John", "Sarah", "Mike"], timestamp: new Date() }); }); app.listen(PORT, () => { console.log(`✅ Server running: http://localhost:${PORT}`); });
PART 6: ADDING HTML FILES (PROPER WEBSITE)
Step 1: Create Folder Structure
my-first-project/ ├── node_modules/ ├── public/ │ ├── index.html │ ├── about.html │ └── styles.css ├── server.js └── package.json
Step 2: Create HTML Files
public/index.html:
<!DOCTYPE html> <html> <head> <title>My Website</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <nav> <a href="/">Home</a> <a href="/about">About</a> <a href="/contact">Contact</a> </nav> <h1>Welcome to My Homepage!</h1> <p>This is served by Express.js</p> <button id="loadData">Load Data</button> <div id="data"></div> <script> document.getElementById('loadData').addEventListener('click', async () => { const response = await fetch('/api/data'); const data = await response.json(); document.getElementById('data').innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`; }); </script> </body> </html>
public/about.html:
<!DOCTYPE html> <html> <head> <title>About Us</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <nav> <a href="/">Home</a> <a href="/about">About</a> <a href="/contact">Contact</a> </nav> <h1>About Our Company</h1> <p>We teach beginners how to code!</p> </body> </html>
public/styles.css:
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; } nav { background: #333; padding: 15px; margin-bottom: 20px; } nav a { color: white; margin-right: 15px; text-decoration: none; } nav a:hover { text-decoration: underline; } button { background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; } button:hover { background: #0056b3; }
Step 3: Update server.js
const express = require('express'); const app = express(); const PORT = 3000; // Serve static files (HTML, CSS, images) app.use(express.static('public')); // API endpoint app.get('/api/data', (req, res) => { res.json({ message: "Hello from the server!", serverTime: new Date().toISOString(), status: "active" }); }); // Custom route for about page app.get('/about', (req, res) => { res.sendFile(__dirname + '/public/about.html'); }); // Handle 404 errors app.use((req, res) => { res.status(404).send(` <h1>404 - Page Not Found</h1> <p>The page you're looking for doesn't exist.</p> <a href="/">Go back home</a> `); }); app.listen(PORT, () => { console.log(`✅ Server running: http://localhost:${PORT}`); console.log(`📁 Static files served from: /public`); console.log(`🔗 Homepage: http://localhost:${PORT}/index.html`); });
PART 7: ADDING FORMS & PROCESSING DATA
Step 1: Add Contact Form
public/contact.html:
<!DOCTYPE html> <html> <head> <title>Contact Us</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <nav> <a href="/">Home</a> <a href="/about">About</a> <a href="/contact">Contact</a> </nav> <h1>Contact Us</h1> <form id="contactForm"> <div> <label>Name:</label> <input type="text" name="name" required> </div> <div> <label>Email:</label> <input type="email" name="email" required> </div> <div> <label>Message:</label> <textarea name="message" rows="4" required></textarea> </div> <button type="submit">Send Message</button> </form> <div id="response"></div> <script> document.getElementById('contactForm').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData); try { const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); document.getElementById('response').innerHTML = `<p style="color: green;">${result.message}</p>`; e.target.reset(); } catch (error) { document.getElementById('response').innerHTML = `<p style="color: red;">Error: ${error.message}</p>`; } }); </script> </body> </html>
Step 2: Update server.js to Handle Form Data
const express = require('express'); const app = express(); const PORT = 3000; // Middleware to parse JSON and form data app.use(express.json()); // For JSON data app.use(express.urlencoded({ extended: true })); // For form data // Serve static files app.use(express.static('public')); // API endpoint for contact form app.post('/api/contact', (req, res) => { console.log('Form submitted:', req.body); // In a real app, you would save to database here res.json({ message: `Thank you ${req.body.name}! We received your message.`, receivedData: req.body }); }); // API endpoint to get data app.get('/api/data', (req, res) => { res.json({ message: "Hello from the server!", serverTime: new Date().toISOString(), status: "active" }); }); // Routes for HTML pages app.get('/', (req, res) => { res.sendFile(__dirname + '/public/index.html'); }); app.get('/about', (req, res) => { res.sendFile(__dirname + '/public/about.html'); }); app.get('/contact', (req, res) => { res.sendFile(__dirname + '/public/contact.html'); }); // Handle 404 errors app.use((req, res) => { res.status(404).send(` <h1>404 - Page Not Found</h1> <p>The page you're looking for doesn't exist.</p> <a href="/">Go back home</a> `); }); app.listen(PORT, () => { console.log(`✅ Server running: http://localhost:${PORT}`); console.log('📌 Available pages:'); console.log(` Home: http://localhost:${PORT}/`); console.log(` About: http://localhost:${PORT}/about`); console.log(` Contact: http://localhost:${PORT}/contact`); console.log(` API Data: http://localhost:${PORT}/api/data`); });
PART 8: DEVELOPMENT TOOLS
Install Nodemon (Auto-restart server)
npm install --save-dev nodemon
Update package.json:
{ "scripts": { "start": "node server.js", "dev": "nodemon server.js" } }
Then it looks like{ "name": "my-first-project", "version": "1.0.0", "description": "", "main": "hello.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js", "dev": "nodemon server.js" }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs", "dependencies": { "express": "^5.2.1" }, "devDependencies": { "nodemon": "^3.1.11" } }
Now run with:
npm run devNodemon automatically restarts when you save changes!
Project Structure Final
my-first-project/ ├── node_modules/ ├── public/ │ ├── index.html │ ├── about.html │ ├── contact.html │ └── styles.css ├── server.js ├── package.json └── README.md
PART 9: TESTING YOUR APPLICATION
Start the server:
npm run devVisit these URLs in your browser:
http://localhost:3000/- Homepagehttp://localhost:3000/about- About pagehttp://localhost:3000/contact- Contact formhttp://localhost:3000/api/data- Raw JSON data
Test the contact form:
Fill out the form on
/contactClick submit
Check browser console for response
Check terminal for server logs
Complete PostgreSQL + Node.js/Express Guide for Beginners
PART 1: POSTGRESQL INSTALLATION
Option A: Install PostgreSQL Locally
Windows:
Download from postgresql.org/download/windows/
Run installer, remember password you set for "postgres" user
Check if installed: Search for "pgAdmin" or "SQL Shell" in Start Menu
Mac:
# Using Homebrew (recommended) brew install postgresql brew services start postgresql # Or download from postgresql.org/download/macosx/
Linux (Ubuntu/Debian):
sudo apt update sudo apt install postgresql postgresql-contrib sudo systemctl start postgresql
Option B: Use Cloud Database (Easier for Beginners)
Sign up for a free account:
Neon.tech (Recommended - free tier)
Supabase (Free tier)
ElephantSQL (Free tier)
Get your connection string:
postgresql://username:password@host:port/database
PART 2: SET UP YOUR PROJECT
Step 1: Create Project Structure
mkdir node-postgres-tutorial cd node-postgres-tutorial npm init -y
Step 2: Install Required Packages
npm install express pg dotenv npm install --save-dev nodemon
What each package does:
express: Web frameworkpg: PostgreSQL client for Node.jsdotenv: Load environment variables from .env filenodemon: Auto-restart server on changes
Step 3: Basic Project Structure
node-postgres-tutorial/ ├── node_modules/ ├── src/ │ ├── config/ │ │ └── database.js │ ├── controllers/ │ │ └── userController.js │ ├── routes/ │ │ └── userRoutes.js │ └── server.js ├── .env ├── .gitignore ├── package.json └── README.md
PART 3: DATABASE CONNECTION
Step 1: Create .env file
# Database Connection DB_HOST=localhost DB_PORT=5432 DB_USER=postgres DB_PASSWORD=yourpassword DB_NAME=mydatabase # Server PORT=3000 NODE_ENV=development # For cloud databases, use connection string: # DATABASE_URL=postgresql://username:password@host:port/database
Step 2: Create database configuration
src/config/database.js:
const { Pool } = require('pg'); require('dotenv').config(); // Create a connection pool const pool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, // For cloud databases, use connection string: // connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false }); // Test connection pool.connect((err, client, release) => { if (err) { console.error('❌ Error connecting to PostgreSQL:', err.message); } else { console.log('✅ Successfully connected to PostgreSQL database'); release(); } }); module.exports = { query: (text, params) => pool.query(text, params), pool };
PART 4: INITIAL DATABASE SETUP
Step 1: Create SQL Script
Create database-setup.sql:
-- Create database (run this in PostgreSQL shell or pgAdmin first) -- CREATE DATABASE mydatabase; -- Connect to your database first, then run: -- Create users table CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, age INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Create posts table CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Insert sample data INSERT INTO users (username, email, password, age) VALUES ('john_doe', 'john@example.com', 'hashed_password_123', 25), ('jane_smith', 'jane@example.com', 'hashed_password_456', 30), ('bob_wilson', 'bob@example.com', 'hashed_password_789', 22) ON CONFLICT (username) DO NOTHING; INSERT INTO posts (user_id, title, content) VALUES (1, 'My First Post', 'Hello everyone, this is my first post!'), (1, 'Learning PostgreSQL', 'PostgreSQL is amazing for web apps.'), (2, 'Node.js Tips', 'Here are some Node.js tips for beginners.') ON CONFLICT DO NOTHING; -- Create indexes for better performance CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);
Step 2: How to Run SQL Script
Using psql (Command Line):
# Connect to PostgreSQL psql -U postgres -h localhost # Create database CREATE DATABASE mydatabase; # Connect to database \c mydatabase # Run SQL file \i database-setup.sql
with full path
\i 'C:/my-first-project/node-postgres-tutorial/database-setup.sql'
Using pgAdmin (GUI):
Open pgAdmin
Connect to your server
Right-click on Databases → Create → Database
Name it "mydatabase"
Right-click on "mydatabase" → Query Tool
Copy-paste SQL from
database-setup.sqland run
PART 5: BASIC SERVER WITH DATABASE
Step 1: Create Main Server File
src/server.js:
const express = require('express'); require('dotenv').config(); const db = require('./config/database'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Basic route app.get('/', (req, res) => { res.json({ message: 'Welcome to Node.js + PostgreSQL API', endpoints: { users: '/api/users', userById: '/api/users/:id', posts: '/api/posts' }, database: 'PostgreSQL', status: 'connected' }); }); // Test database connection app.get('/health', async (req, res) => { try { const result = await db.query('SELECT NOW()'); res.json({ status: 'healthy', database: 'connected', timestamp: result.rows[0].now }); } catch (error) { res.status(500).json({ status: 'unhealthy', database: 'disconnected', error: error.message }); } }); // Start server app.listen(PORT, () => { console.log(`🚀 Server running on http://localhost:${PORT}`); console.log(`📊 Database: PostgreSQL`); console.log(`⚡ Environment: ${process.env.NODE_ENV}`); });
Step 2: Update package.json
{ "name": "node-postgres-tutorial", "version": "1.0.0", "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", "setup-db": "psql -U postgres -f database-setup.sql" }, "dependencies": { "express": "^4.18.2", "pg": "^8.11.0", "dotenv": "^16.3.1" }, "devDependencies": { "nodemon": "^3.0.1" } }
Looks like this{ "name": "node-postgres-tutorial", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", "setup-db": "psql -U postgres -f database-setup.sql", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs", "dependencies": { "dotenv": "^17.2.3", "express": "^5.2.1", "pg": "^8.16.3" }, "devDependencies": { "nodemon": "^3.1.11" }}Step 3: Run Your Server
npm run devVisit http://localhost:3000/health to test database connection!
PART 6: CRUD OPERATIONS - USERS
Step 1: Create User Controller
src/controllers/userController.js:
const db = require('../config/database'); // Get all users const getAllUsers = async (req, res) => { try { const result = await db.query( 'SELECT id, username, email, age, created_at FROM users ORDER BY id' ); res.json({ success: true, count: result.rowCount, users: result.rows }); } catch (error) { console.error('Error fetching users:', error); res.status(500).json({ success: false, message: 'Error fetching users', error: error.message }); } }; // Get single user by ID const getUserById = async (req, res) => { try { const { id } = req.params; const result = await db.query( 'SELECT id, username, email, age, created_at FROM users WHERE id = $1', [id] ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'User not found' }); } res.json({ success: true, user: result.rows[0] }); } catch (error) { console.error('Error fetching user:', error); res.status(500).json({ success: false, message: 'Error fetching user', error: error.message }); } }; // Create new user const createUser = async (req, res) => { try { const { username, email, password, age } = req.body; // Basic validation if (!username || !email || !password) { return res.status(400).json({ success: false, message: 'Username, email, and password are required' }); } const result = await db.query( `INSERT INTO users (username, email, password, age) VALUES ($1, $2, $3, $4) RETURNING id, username, email, age, created_at`, [username, email, password, age || null] ); res.status(201).json({ success: true, message: 'User created successfully', user: result.rows[0] }); } catch (error) { console.error('Error creating user:', error); // Handle duplicate username/email if (error.code === '23505') { // Unique violation return res.status(409).json({ success: false, message: 'Username or email already exists' }); } res.status(500).json({ success: false, message: 'Error creating user', error: error.message }); } }; // Update user const updateUser = async (req, res) => { try { const { id } = req.params; const { username, email, age } = req.body; // Check if user exists const userCheck = await db.query('SELECT id FROM users WHERE id = $1', [id]); if (userCheck.rows.length === 0) { return res.status(404).json({ success: false, message: 'User not found' }); } const result = await db.query( `UPDATE users SET username = COALESCE($1, username), email = COALESCE($2, email), age = COALESCE($3, age), updated_at = CURRENT_TIMESTAMP WHERE id = $4 RETURNING id, username, email, age, updated_at`, [username, email, age, id] ); res.json({ success: true, message: 'User updated successfully', user: result.rows[0] }); } catch (error) { console.error('Error updating user:', error); res.status(500).json({ success: false, message: 'Error updating user', error: error.message }); } }; // Delete user const deleteUser = async (req, res) => { try { const { id } = req.params; const result = await db.query( 'DELETE FROM users WHERE id = $1 RETURNING id', [id] ); if (result.rowCount === 0) { return res.status(404).json({ success: false, message: 'User not found' }); } res.json({ success: true, message: 'User deleted successfully' }); } catch (error) { console.error('Error deleting user:', error); res.status(500).json({ success: false, message: 'Error deleting user', error: error.message }); } }; // Search users const searchUsers = async (req, res) => { try { const { query } = req.query; if (!query) { return res.status(400).json({ success: false, message: 'Search query is required' }); } const searchPattern = `%${query}%`; const result = await db.query( `SELECT id, username, email, age, created_at FROM users WHERE username ILIKE $1 OR email ILIKE $1 ORDER BY username`, [searchPattern] ); res.json({ success: true, count: result.rowCount, users: result.rows }); } catch (error) { console.error('Error searching users:', error); res.status(500).json({ success: false, message: 'Error searching users', error: error.message }); } }; module.exports = { getAllUsers, getUserById, createUser, updateUser, deleteUser, searchUsers };
Step 2: Create User Routes
src/routes/userRoutes.js:
const express = require('express'); const router = express.Router(); const { getAllUsers, getUserById, createUser, updateUser, deleteUser, searchUsers } = require('../controllers/userController'); // GET all users router.get('/', getAllUsers); // GET single user router.get('/:id', getUserById); // POST create new user router.post('/', createUser); // PUT update user router.put('/:id', updateUser); // DELETE user router.delete('/:id', deleteUser); // SEARCH users router.get('/search', searchUsers); module.exports = router;
Step 3: Update Server to Use Routes
Update src/server.js:
const express = require('express'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3000; // Import routes const userRoutes = require('./routes/userRoutes'); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Routes app.use('/api/users', userRoutes); // Basic routes app.get('/', (req, res) => { res.json({ message: 'Welcome to Node.js + PostgreSQL API', endpoints: { users: { getAll: 'GET /api/users', getSingle: 'GET /api/users/:id', create: 'POST /api/users', update: 'PUT /api/users/:id', delete: 'DELETE /api/users/:id', search: 'GET /api/users/search?query=term' } } }); }); // Health check app.get('/health', async (req, res) => { try { const db = require('./config/database'); const result = await db.query('SELECT NOW()'); res.json({ status: 'healthy', database: 'connected', timestamp: result.rows[0].now }); } catch (error) { res.status(500).json({ status: 'unhealthy', database: 'disconnected', error: error.message }); } }); // 404 handler app.use((err, req, res, next) => {
res.status(404).json({ success: false, message: `Route ${req.originalUrl} not found` }); }); // Error handler app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ success: false, message: 'Something went wrong!', error: process.env.NODE_ENV === 'development' ? err.message : undefined }); }); // Start server app.listen(PORT, () => { console.log(`🚀 Server running on http://localhost:${PORT}`); console.log(`📊 Database: PostgreSQL`); console.log(`⚡ Environment: ${process.env.NODE_ENV}`); console.log(`📝 API Documentation: http://localhost:${PORT}`); });
PART 7: TESTING THE API
Using Browser:
http://localhost:3000/- API documentationhttp://localhost:3000/health- Health checkhttp://localhost:3000/api/users- Get all usershttp://localhost:3000/api/users/1- Get user by IDhttp://localhost:3000/api/users/search?query=john- Search users
Using curl (Command Line):
# Get all users curl http://localhost:3000/api/users # Get user by ID curl http://localhost:3000/api/users/1 # Create new user curl -X POST http://localhost:3000/api/users \ -H "Content-Type: application/json" \ -d '{"username":"test_user","email":"test@example.com","password":"test123","age":28}' # Update user curl -X PUT http://localhost:3000/api/users/1 \ -H "Content-Type: application/json" \ -d '{"email":"newemail@example.com"}' # Delete user curl -X DELETE http://localhost:3000/api/users/4
Using Postman or Thunder Client:
Install Thunder Client (VS Code extension) or Postman
Create requests for each endpoint
Test with different data
PART 8: ADVANCED FEATURES
1. Pagination
Update userController.js:
const getAllUsersPaginated = async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const offset = (page - 1) * limit; // Get total count const countResult = await db.query('SELECT COUNT(*) FROM users'); const total = parseInt(countResult.rows[0].count); const totalPages = Math.ceil(total / limit); // Get paginated users const result = await db.query( `SELECT id, username, email, age, created_at FROM users ORDER BY id LIMIT $1 OFFSET $2`, [limit, offset] ); res.json({ success: true, pagination: { page, limit, total, totalPages, hasNext: page < totalPages, hasPrev: page > 1 }, users: result.rows }); } catch (error) { console.error('Error fetching users:', error); res.status(500).json({ success: false, message: 'Error fetching users', error: error.message }); } };
Usage: GET /api/users?page=1&limit=5
2. Transactions
const createUserWithTransaction = async (req, res) => { const client = await db.pool.connect(); try { await client.query('BEGIN'); const { username, email, password, age } = req.body; // Insert user const userResult = await client.query( `INSERT INTO users (username, email, password, age) VALUES ($1, $2, $3, $4) RETURNING id`, [username, email, password, age] ); const userId = userResult.rows[0].id; // Create default post for user await client.query( `INSERT INTO posts (user_id, title, content) VALUES ($1, $2, $3)`, [userId, 'Welcome Post', 'Welcome to our platform!'] ); await client.query('COMMIT'); res.status(201).json({ success: true, message: 'User created with welcome post', userId }); } catch (error) { await client.query('ROLLBACK'); console.error('Transaction error:', error); res.status(500).json({ success: false, message: 'Transaction failed', error: error.message }); } finally { client.release(); } };
3. Connection Pool Monitoring
src/config/database.js - Extended:
const { Pool } = require('pg'); require('dotenv').config(); const pool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, max: 20, // Maximum number of clients in the pool idleTimeoutMillis: 30000, // How long a client is allowed to remain idle connectionTimeoutMillis: 2000, // How long to wait for a connection }); // Log pool events pool.on('connect', () => { console.log('New client connected to PostgreSQL pool'); }); pool.on('error', (err, client) => { console.error('Unexpected error on idle client', err); }); pool.on('remove', () => { console.log('Client removed from pool'); }); // Add pool stats endpoint const getPoolStats = () => { return { totalCount: pool.totalCount, idleCount: pool.idleCount, waitingCount: pool.waitingCount }; }; module.exports = { query: (text, params) => pool.query(text, params), pool, getPoolStats };
4. Environment-specific Configurations
.env.development:
DB_HOST=localhost DB_PORT=5432 DB_USER=postgres DB_PASSWORD=dev_password DB_NAME=mydatabase_dev PORT=3000 NODE_ENV=development
.env.production:
DATABASE_URL=postgresql://username:password@host:port/production_db PORT=80 NODE_ENV=production
Update package.json:
{ "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", "dev:env": "NODE_ENV=development nodemon src/server.js", "prod": "NODE_ENV=production node src/server.js" } }
PART 9: TROUBLESHOOTING
Common Errors & Solutions:
"password authentication failed for user"
# Check your .env file # Reset password: sudo -u postgres psql \password postgres
"database does not exist"
# Create database sudo -u postgres createdb mydatabase
"Connection refused"
# Check if PostgreSQL is running sudo systemctl status postgresql # Start if stopped sudo systemctl start postgresql
"Too many clients already"
// Increase pool size in database.js max: 50, // Increase this number
Debug Queries:
// Add to database.js for debugging const queryWithLog = async (text, params) => { const start = Date.now(); const res = await pool.query(text, params); const duration = Date.now() - start; console.log('Executed query', { text, duration, rows: res.rowCount }); return res; };
PART 10: DEPLOYMENT
Option 1: Railway.app (Easiest for Beginners)
Push code to GitHub
Sign up at railway.app
Create new project → Deploy from GitHub repo
Add PostgreSQL plugin
Railway automatically sets
DATABASE_URLenvironment variable
Option 2: Heroku
# Install Heroku CLI heroku login heroku create your-app-name heroku addons:create heroku-postgresql:hobby-dev git push heroku main
COMPLETE EXAMPLE PROJECT
Download the complete project structure:
# Create project with all files mkdir complete-postgres-project cd complete-postgres-project # Initialize and install npm init -y npm install express pg dotenv # Create all the files as shown above
NEXT STEPS TO LEARN:
Authentication (JWT tokens)
Data Validation (with Joi or Zod)
ORM/Query Builders (Prisma, Sequelize, or Knex.js)
Database Migrations
Performance Optimization (Indexing, Query optimization)
Backup & Recovery
QUICK REFERENCE - All Commands:
# Project setup npm init -y npm install express pg dotenv npm install --save-dev nodemon # Start development server npm run dev # Database commands psql -U postgres # Connect to PostgreSQL \l # List databases \c database_name # Connect to database \dt # List tables \q # Quit # API Testing with curl curl http://localhost:3000/api/users curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d '{"username":"test"}'
Remember: Always use environment variables for sensitive data, never commit .env to GitHub!
Complete User Authentication System for Beginners
PART 1: AUTHENTICATION CONCEPTS EXPLAINED SIMPLY
What is Authentication?
Register: Create new account (sign up)
Login: Access existing account (sign in)
Logout: End your session
Protected Routes: Pages only logged-in users can see
Tools We'll Use:
bcryptjs - Hash passwords (never store plain passwords!)
jsonwebtoken (JWT) - Create secure tokens for sessions
express-validator - Validate user input
cookie-parser - Handle cookies for web browsers
PART 2: PROJECT SETUP
Step 1: Create New Project
mkdir auth-tutorial cd auth-tutorial npm init -y
Step 2: Install All Packages
npm install express pg dotenv bcryptjs jsonwebtoken cookie-parser express-validator cors npm install --save-dev nodemon
Step 3: Project Structure
auth-tutorial/ ├── node_modules/ ├── src/ │ ├── config/ │ │ └── database.js │ ├── controllers/ │ │ └── authController.js │ ├── middleware/ │ │ ├── authMiddleware.js │ │ └── validationMiddleware.js │ ├── routes/ │ │ └── authRoutes.js │ ├── utils/ │ │ └── jwtUtils.js │ └── server.js ├── .env ├── .gitignore ├── package.json └── README.md
Step 4: Environment Variables (.env)
# Server PORT=3000 NODE_ENV=development # Database DB_HOST=localhost DB_PORT=5432 DB_USER=postgres DB_PASSWORD=yourpassword DB_NAME=auth_demo # JWT Secrets (NEVER share these!) JWT_SECRET=your_super_secret_jwt_key_change_this_in_production JWT_REFRESH_SECRET=your_super_secret_refresh_key_change_this_too JWT_EXPIRES_IN=15m JWT_REFRESH_EXPIRES_IN=7d # Security BCRYPT_SALT_ROUNDS=10
PART 3: DATABASE SETUP
Step 1: Create Database Schema
Create database-setup.sql:
-- Create database (run this first in PostgreSQL) -- CREATE DATABASE auth_demo; -- Users table CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, first_name VARCHAR(50), last_name VARCHAR(50), is_active BOOLEAN DEFAULT true, is_verified BOOLEAN DEFAULT false, role VARCHAR(20) DEFAULT 'user', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- User profiles table (optional, for additional user info) CREATE TABLE IF NOT EXISTS user_profiles ( id SERIAL PRIMARY KEY, user_id INTEGER UNIQUE REFERENCES users(id) ON DELETE CASCADE, bio TEXT, avatar_url VARCHAR(500), phone VARCHAR(20), birthdate DATE, address TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Password reset tokens CREATE TABLE IF NOT EXISTS password_reset_tokens ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(255) NOT NULL, expires_at TIMESTAMP NOT NULL, used BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Email verification tokens CREATE TABLE IF NOT EXISTS verification_tokens ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(255) NOT NULL, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- User sessions (for refresh tokens) CREATE TABLE IF NOT EXISTS user_sessions ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, refresh_token VARCHAR(500) NOT NULL, user_agent TEXT, ip_address VARCHAR(45), expires_at TIMESTAMP NOT NULL, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Create indexes for better performance CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_password_reset_tokens_token ON password_reset_tokens(token); CREATE INDEX idx_verification_tokens_token ON verification_tokens(token); CREATE INDEX idx_user_sessions_refresh_token ON user_sessions(refresh_token); -- Insert admin user (password: admin123) -- You can run this after setting up the registration
Step 2: Database Configuration
src/config/database.js:
const { Pool } = require('pg'); require('dotenv').config(); const pool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // Test connection pool.connect((err, client, release) => { if (err) { console.error('❌ Error connecting to PostgreSQL:', err.message); } else { console.log('✅ Connected to PostgreSQL database'); release(); } }); module.exports = { query: (text, params) => pool.query(text, params), pool };
Step 2: Password Utilities
src/utils/passwordUtils.js:
const bcrypt = require('bcryptjs'); // Hash password const hashPassword = async (password) => { const saltRounds = parseInt(process.env.BCRYPT_SALT_ROUNDS) || 10; return await bcrypt.hash(password, saltRounds); }; // Compare password with hash const comparePassword = async (password, hash) => { return await bcrypt.compare(password, hash); }; // Generate random password (for password reset) const generateRandomPassword = (length = 12) => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'; let password = ''; for (let i = 0; i < length; i++) { password += chars.charAt(Math.floor(Math.random() * chars.length)); } return password; }; module.exports = { hashPassword, comparePassword, generateRandomPassword };
PART 5: VALIDATION MIDDLEWARE
src/middleware/validationMiddleware.js:
const { body, validationResult } = require('express-validator'); // Common validation rules const validationRules = { // Register validation register: [ body('email') .isEmail().normalizeEmail().withMessage('Please provide a valid email') .notEmpty().withMessage('Email is required'), body('username') .trim() .notEmpty().withMessage('Username is required') .isLength({ min: 3, max: 50 }).withMessage('Username must be between 3-50 characters') .matches(/^[a-zA-Z0-9_]+$/).withMessage('Username can only contain letters, numbers, and underscores'), body('password') .notEmpty().withMessage('Password is required') .isLength({ min: 6 }).withMessage('Password must be at least 6 characters') .matches(/[A-Z]/).withMessage('Password must contain at least one uppercase letter') .matches(/[a-z]/).withMessage('Password must contain at least one lowercase letter') .matches(/[0-9]/).withMessage('Password must contain at least one number'), body('confirmPassword') .notEmpty().withMessage('Please confirm your password') .custom((value, { req }) => value === req.body.password) .withMessage('Passwords do not match'), body('firstName') .optional() .trim() .isLength({ min: 2, max: 50 }).withMessage('First name must be between 2-50 characters'), body('lastName') .optional() .trim() .isLength({ min: 2, max: 50 }).withMessage('Last name must be between 2-50 characters'), ], // Login validation login: [ body('email') .optional() .isEmail().normalizeEmail().withMessage('Please provide a valid email'), body('username') .optional() .trim() .notEmpty().withMessage('Username or email is required'), body('password') .notEmpty().withMessage('Password is required'), // Custom validation to ensure either email or username is provided body().custom((value, { req }) => { if (!req.body.email && !req.body.username) { throw new Error('Either email or username is required'); } return true; }), ], // Change password validation changePassword: [ body('currentPassword') .notEmpty().withMessage('Current password is required'), body('newPassword') .notEmpty().withMessage('New password is required') .isLength({ min: 6 }).withMessage('Password must be at least 6 characters') .matches(/[A-Z]/).withMessage('Password must contain at least one uppercase letter') .matches(/[a-z]/).withMessage('Password must contain at least one lowercase letter') .matches(/[0-9]/).withMessage('Password must contain at least one number') .custom((value, { req }) => value !== req.body.currentPassword) .withMessage('New password must be different from current password'), body('confirmNewPassword') .notEmpty().withMessage('Please confirm your new password') .custom((value, { req }) => value === req.body.newPassword) .withMessage('Passwords do not match'), ], // Forgot password validation forgotPassword: [ body('email') .isEmail().normalizeEmail().withMessage('Please provide a valid email') .notEmpty().withMessage('Email is required'), ], // Reset password validation resetPassword: [ body('password') .notEmpty().withMessage('Password is required') .isLength({ min: 6 }).withMessage('Password must be at least 6 characters') .matches(/[A-Z]/).withMessage('Password must contain at least one uppercase letter') .matches(/[a-z]/).withMessage('Password must contain at least one lowercase letter') .matches(/[0-9]/).withMessage('Password must contain at least one number'), body('confirmPassword') .notEmpty().withMessage('Please confirm your password') .custom((value, { req }) => value === req.body.password) .withMessage('Passwords do not match'), ], // Update profile validation updateProfile: [ body('firstName') .optional() .trim() .isLength({ min: 2, max: 50 }).withMessage('First name must be between 2-50 characters'), body('lastName') .optional() .trim() .isLength({ min: 2, max: 50 }).withMessage('Last name must be between 2-50 characters'), body('username') .optional() .trim() .isLength({ min: 3, max: 50 }).withMessage('Username must be between 3-50 characters') .matches(/^[a-zA-Z0-9_]+$/).withMessage('Username can only contain letters, numbers, and underscores'), body('email') .optional() .isEmail().normalizeEmail().withMessage('Please provide a valid email'), ], }; // Validation middleware const validate = (validations) => { return async (req, res, next) => { // Run all validations await Promise.all(validations.map(validation => validation.run(req))); // Check for errors const errors = validationResult(req); if (errors.isEmpty()) { return next(); } // Format errors const formattedErrors = errors.array().map(error => ({ field: error.path, message: error.msg })); res.status(400).json({ success: false, message: 'Validation failed', errors: formattedErrors }); }; }; module.exports = { validationRules, validate };
PART 6: AUTHENTICATION MIDDLEWARE
src/middleware/authMiddleware.js:
const { verifyAccessToken } = require('../utils/jwtUtils'); const db = require('../config/database'); // Middleware to check if user is authenticated const authenticate = async (req, res, next) => { try { // Get token from header or cookie let token = req.headers.authorization || req.cookies.access_token; if (!token) { return res.status(401).json({ success: false, message: 'Access denied. No token provided.' }); } // Remove 'Bearer ' prefix if present if (token.startsWith('Bearer ')) { token = token.slice(7); } // Verify token const decoded = verifyAccessToken(token); if (!decoded) { return res.status(401).json({ success: false, message: 'Invalid or expired token' }); } // Check if user exists and is active const userResult = await db.query( 'SELECT id, email, username, role, is_active, is_verified FROM users WHERE id = $1', [decoded.userId] ); if (userResult.rows.length === 0) { return res.status(401).json({ success: false, message: 'User not found' }); } if (!userResult.rows[0].is_active) { return res.status(401).json({ success: false, message: 'Account is deactivated' }); } // Attach user to request object req.user = userResult.rows[0]; next(); } catch (error) { console.error('Authentication error:', error); res.status(500).json({ success: false, message: 'Authentication failed' }); } }; // Middleware to check if user is verified const requireVerified = (req, res, next) => { if (!req.user.is_verified) { return res.status(403).json({ success: false, message: 'Please verify your email address first' }); } next(); }; // Middleware to check user role const requireRole = (...roles) => { return (req, res, next) => { if (!req.user) { return res.status(401).json({ success: false, message: 'Authentication required' }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ success: false, message: 'Insufficient permissions' }); } next(); }; }; // Middleware to check if user is the owner or has admin role const requireOwnershipOrAdmin = (paramName = 'id') => { return async (req, res, next) => { try { const resourceId = req.params[paramName]; const userId = req.user.id; const userRole = req.user.role; // Admins can access anything if (userRole === 'admin') { return next(); } // User can only access their own resources if (parseInt(resourceId) !== userId) { return res.status(403).json({ success: false, message: 'You can only access your own resources' }); } next(); } catch (error) { console.error('Ownership check error:', error); res.status(500).json({ success: false, message: 'Permission check failed' }); } }; }; module.exports = { authenticate, requireVerified, requireRole, requireOwnershipOrAdmin };
PART 7: AUTHENTICATION CONTROLLER
src/controllers/authController.js:
const db = require('../config/database'); const { hashPassword, comparePassword } = require('../utils/passwordUtils'); const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../utils/jwtUtils'); // Helper function to get user by email or username const getUserByIdentifier = async (identifier) => { // Check if identifier is email if (identifier.includes('@')) { const result = await db.query( 'SELECT * FROM users WHERE email = $1', [identifier] ); return result.rows[0]; } // Otherwise treat as username const result = await db.query( 'SELECT * FROM users WHERE username = $1', [identifier] ); return result.rows[0]; }; // Register User const register = async (req, res) => { try { const { email, username, password, firstName, lastName } = req.body; // Check if user already exists const existingUser = await db.query( 'SELECT id FROM users WHERE email = $1 OR username = $2', [email, username] ); if (existingUser.rows.length > 0) { return res.status(409).json({ success: false, message: 'User with this email or username already exists' }); } // Hash password const passwordHash = await hashPassword(password); // Create user const result = await db.query( `INSERT INTO users (email, username, password_hash, first_name, last_name) VALUES ($1, $2, $3, $4, $5) RETURNING id, email, username, first_name, last_name, role, created_at`, [email, username, passwordHash, firstName, lastName] ); const user = result.rows[0]; // Generate tokens const accessToken = generateAccessToken(user.id, user.email, user.role); const refreshToken = generateRefreshToken(user.id); // Store refresh token in database await db.query( `INSERT INTO user_sessions (user_id, refresh_token, user_agent, ip_address, expires_at) VALUES ($1, $2, $3, $4, NOW() + INTERVAL '7 days')`, [user.id, refreshToken, req.headers['user-agent'], req.ip] ); // Set cookies (for web browsers) res.cookie('access_token', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 15 * 60 * 1000 // 15 minutes }); res.cookie('refresh_token', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); res.status(201).json({ success: true, message: 'Registration successful', user: { id: user.id, email: user.email, username: user.username, firstName: user.first_name, lastName: user.last_name, role: user.role }, tokens: { accessToken, refreshToken } }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ success: false, message: 'Registration failed', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; // Login User const login = async (req, res) => { try { const { email, username, password } = req.body; const identifier = email || username; // Get user const user = await getUserByIdentifier(identifier); if (!user) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Check if account is active if (!user.is_active) { return res.status(401).json({ success: false, message: 'Account is deactivated. Please contact support.' }); } // Verify password const isValidPassword = await comparePassword(password, user.password_hash); if (!isValidPassword) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Generate tokens const accessToken = generateAccessToken(user.id, user.email, user.role); const refreshToken = generateRefreshToken(user.id); // Store refresh token in database await db.query( `INSERT INTO user_sessions (user_id, refresh_token, user_agent, ip_address, expires_at) VALUES ($1, $2, $3, $4, NOW() + INTERVAL '7 days')`, [user.id, refreshToken, req.headers['user-agent'], req.ip] ); // Update last login (you might want to add this field to users table) await db.query( 'UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = $1', [user.id] ); // Set cookies res.cookie('access_token', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 15 * 60 * 1000 }); res.cookie('refresh_token', refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 }); res.json({ success: true, message: 'Login successful', user: { id: user.id, email: user.email, username: user.username, firstName: user.first_name, lastName: user.last_name, role: user.role, isVerified: user.is_verified }, tokens: { accessToken, refreshToken } }); } catch (error) { console.error('Login error:', error); res.status(500).json({ success: false, message: 'Login failed', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }; // Logout User const logout = async (req, res) => { try { const refreshToken = req.cookies.refresh_token || req.body.refreshToken; if (refreshToken) { // Remove refresh token from database await db.query( 'UPDATE user_sessions SET is_active = false WHERE refresh_token = $1', [refreshToken] ); } // Clear cookies res.clearCookie('access_token'); res.clearCookie('refresh_token'); res.json({ success: true, message: 'Logged out successfully' }); } catch (error) { console.error('Logout error:', error); res.status(500).json({ success: false, message: 'Logout failed' }); } }; // Refresh Access Token const refreshToken = async (req, res) => { try { const refreshToken = req.cookies.refresh_token || req.body.refreshToken; if (!refreshToken) { return res.status(401).json({ success: false, message: 'Refresh token required' }); } // Verify refresh token const decoded = verifyRefreshToken(refreshToken); if (!decoded) { return res.status(401).json({ success: false, message: 'Invalid or expired refresh token' }); } // Check if refresh token exists in database and is active const sessionResult = await db.query( `SELECT us.*, u.email, u.role, u.is_active FROM user_sessions us JOIN users u ON us.user_id = u.id WHERE us.refresh_token = $1 AND us.is_active = true AND us.expires_at > NOW()`, [refreshToken] ); if (sessionResult.rows.length === 0) { return res.status(401).json({ success: false, message: 'Invalid session' }); } const session = sessionResult.rows[0]; // Check if user is active if (!session.is_active) { return res.status(401).json({ success: false, message: 'Account is deactivated' }); } // Generate new access token const newAccessToken = generateAccessToken(session.user_id, session.email, session.role); // Update cookie res.cookie('access_token', newAccessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 15 * 60 * 1000 }); res.json({ success: true, accessToken: newAccessToken }); } catch (error) { console.error('Refresh token error:', error); res.status(500).json({ success: false, message: 'Token refresh failed' }); } }; // Get Current User Profile const getProfile = async (req, res) => { try { const userId = req.user.id; const result = await db.query( `SELECT u.id, u.email, u.username, u.first_name, u.last_name, u.role, u.is_verified, u.created_at, up.bio, up.avatar_url, up.phone, up.birthdate, up.address FROM users u LEFT JOIN user_profiles up ON u.id = up.user_id WHERE u.id = $1`, [userId] ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'User not found' }); } res.json({ success: true, user: result.rows[0] }); } catch (error) { console.error('Get profile error:', error); res.status(500).json({ success: false, message: 'Failed to fetch profile' }); } }; // Update User Profile const updateProfile = async (req, res) => { try { const userId = req.user.id; const { firstName, lastName, username, email, bio, phone, birthdate, address } = req.body; // Start transaction await db.query('BEGIN'); // Update users table const userUpdateResult = await db.query( `UPDATE users SET first_name = COALESCE($1, first_name), last_name = COALESCE($2, last_name), username = COALESCE($3, username), email = COALESCE($4, email), updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING id, email, username, first_name, last_name, role`, [firstName, lastName, username, email, userId] ); // Update or insert user profile const profileResult = await db.query( `INSERT INTO user_profiles (user_id, bio, phone, birthdate, address) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (user_id) DO UPDATE SET bio = COALESCE($2, user_profiles.bio), phone = COALESCE($3, user_profiles.phone), birthdate = COALESCE($4, user_profiles.birthdate), address = COALESCE($5, user_profiles.address), updated_at = CURRENT_TIMESTAMP RETURNING *`, [userId, bio, phone, birthdate, address] ); await db.query('COMMIT'); res.json({ success: true, message: 'Profile updated successfully', user: userUpdateResult.rows[0], profile: profileResult.rows[0] }); } catch (error) { await db.query('ROLLBACK'); console.error('Update profile error:', error); // Handle unique constraint violations if (error.code === '23505') { return res.status(409).json({ success: false, message: 'Username or email already exists' }); } res.status(500).json({ success: false, message: 'Failed to update profile' }); } }; // Change Password const changePassword = async (req, res) => { try { const userId = req.user.id; const { currentPassword, newPassword } = req.body; // Get current password hash const result = await db.query( 'SELECT password_hash FROM users WHERE id = $1', [userId] ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'User not found' }); } // Verify current password const isValid = await comparePassword(currentPassword, result.rows[0].password_hash); if (!isValid) { return res.status(401).json({ success: false, message: 'Current password is incorrect' }); } // Hash new password const newPasswordHash = await hashPassword(newPassword); // Update password await db.query( 'UPDATE users SET password_hash = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', [newPasswordHash, userId] ); // Invalidate all user sessions (except current) const refreshToken = req.cookies.refresh_token || req.body.refreshToken; if (refreshToken) { await db.query( 'UPDATE user_sessions SET is_active = false WHERE user_id = $1 AND refresh_token != $2', [userId, refreshToken] ); } res.json({ success: true, message: 'Password changed successfully' }); } catch (error) { console.error('Change password error:', error); res.status(500).json({ success: false, message: 'Failed to change password' }); } }; // Forgot Password (Send reset email) const forgotPassword = async (req, res) => { try { const { email } = req.body; // Check if user exists const userResult = await db.query( 'SELECT id, email FROM users WHERE email = $1 AND is_active = true', [email] ); if (userResult.rows.length === 0) { // Don't reveal if user exists (security best practice) return res.json({ success: true, message: 'If an account exists with this email, you will receive a password reset link' }); } const user = userResult.rows[0]; // Generate reset token const resetToken = require('crypto').randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 3600000); // 1 hour from now // Store reset token in database await db.query( `INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES ($1, $2, $3)`, [user.id, resetToken, expiresAt] ); // In production, you would send an email here // For demo purposes, we'll return the token const resetLink = `http://localhost:3000/reset-password?token=${resetToken}`; console.log(`Password reset link for ${email}: ${resetLink}`); res.json({ success: true, message: 'Password reset link sent to email', // In production, remove this line: resetLink: process.env.NODE_ENV === 'development' ? resetLink : undefined }); } catch (error) { console.error('Forgot password error:', error); res.status(500).json({ success: false, message: 'Failed to process password reset request' }); } }; // Reset Password (with token) const resetPassword = async (req, res) => { try { const { token, password } = req.body; // Find valid reset token const tokenResult = await db.query( `SELECT pt.user_id, pt.expires_at FROM password_reset_tokens pt JOIN users u ON pt.user_id = u.id WHERE pt.token = $1 AND pt.used = false AND pt.expires_at > NOW() AND u.is_active = true`, [token] ); if (tokenResult.rows.length === 0) { return res.status(400).json({ success: false, message: 'Invalid or expired reset token' }); } const { user_id, expires_at } = tokenResult.rows[0]; // Hash new password const passwordHash = await hashPassword(password); // Start transaction await db.query('BEGIN'); // Update password await db.query( 'UPDATE users SET password_hash = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', [passwordHash, user_id] ); // Mark token as used await db.query( 'UPDATE password_reset_tokens SET used = true WHERE token = $1', [token] ); // Invalidate all user sessions await db.query( 'UPDATE user_sessions SET is_active = false WHERE user_id = $1', [user_id] ); await db.query('COMMIT'); res.json({ success: true, message: 'Password reset successful. Please login with your new password.' }); } catch (error) { await db.query('ROLLBACK'); console.error('Reset password error:', error); res.status(500).json({ success: false, message: 'Failed to reset password' }); } }; module.exports = { register, login, logout, refreshToken, getProfile, updateProfile, changePassword, forgotPassword, resetPassword };
PART 8: AUTHENTICATION ROUTES
src/routes/authRoutes.js:
const express = require('express'); const router = express.Router(); const { validate, validationRules } = require('../middleware/validationMiddleware'); const { authenticate, requireVerified, requireOwnershipOrAdmin } = require('../middleware/authMiddleware'); const { register, login, logout, refreshToken, getProfile, updateProfile, changePassword, forgotPassword, resetPassword } = require('../controllers/authController'); // Public Routes router.post('/register', validate(validationRules.register), register); router.post('/login', validate(validationRules.login), login); router.post('/refresh-token', refreshToken); router.post('/forgot-password', validate(validationRules.forgotPassword), forgotPassword); router.post('/reset-password', validate(validationRules.resetPassword), resetPassword); // Protected Routes (require authentication) router.post('/logout', authenticate, logout); router.get('/profile', authenticate, getProfile); router.put('/profile', authenticate, validate(validationRules.updateProfile), updateProfile); router.put('/change-password', authenticate, validate(validationRules.changePassword), changePassword); // Admin-only routes (example) router.get('/admin/users', authenticate, requireRole('admin'), async (req, res) => { // This would be in a separate controller res.json({ message: 'Admin user list endpoint' }); }); module.exports = router;
PART 9: MAIN SERVER FILE
src/server.js:
const express = require('express'); const cookieParser = require('cookie-parser'); const cors = require('cors'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3000; // Import routes const authRoutes = require('./routes/authRoutes'); // Middleware app.use(cors({ origin: process.env.NODE_ENV === 'development' ? 'http://localhost:5173' // React/Vite dev server : 'https://yourdomain.com', // Production domain credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); // Logging middleware app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); next(); }); // Routes app.use('/api/auth', authRoutes); // Public test route app.get('/', (req, res) => { res.json({ message: '🔐 Authentication API is running', version: '1.0.0', endpoints: { auth: { register: 'POST /api/auth/register', login: 'POST /api/auth/login', logout: 'POST /api/auth/logout', profile: 'GET /api/auth/profile', refreshToken: 'POST /api/auth/refresh-token', forgotPassword: 'POST /api/auth/forgot-password', resetPassword: 'POST /api/auth/reset-password', changePassword: 'PUT /api/auth/change-password' } }, documentation: 'See README.md for detailed API docs' }); }); // Protected test route app.get('/api/protected', (req, res, next) => { const { authenticate } = require('./middleware/authMiddleware'); authenticate(req, res, () => { res.json({ message: '✅ This is a protected route', user: req.user }); }); }); // Health check app.get('/health', async (req, res) => { try { const db = require('./config/database'); const result = await db.query('SELECT NOW()'); res.json({ status: 'healthy', database: 'connected', timestamp: result.rows[0].now, uptime: process.uptime() }); } catch (error) { res.status(500).json({ status: 'unhealthy', database: 'disconnected', error: error.message }); } }); // 404 handler app.use('*', (req, res) => { res.status(404).json({ success: false, message: `Route ${req.originalUrl} not found` }); }); // Error handler app.use((err, req, res, next) => { console.error('Server error:', err.stack); res.status(500).json({ success: false, message: 'Internal server error', error: process.env.NODE_ENV === 'development' ? err.message : undefined }); }); // Start server app.listen(PORT, () => { console.log(`🚀 Authentication server running on port ${PORT}`); console.log(`⚡ Environment: ${process.env.NODE_ENV}`); console.log(`🌐 Base URL: http://localhost:${PORT}`); console.log(`🔐 Authentication endpoints available at /api/auth/*`); });
PART 10: TESTING THE AUTHENTICATION SYSTEM
Test with curl Commands:
# 1. Register a new user curl -X POST http://localhost:3000/api/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "test@example.com", "username": "testuser", "password": "Password123", "confirmPassword": "Password123", "firstName": "John", "lastName": "Doe" }' # 2. Login curl -X POST http://localhost:3000/api/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "test@example.com", "password": "Password123" }' # Save the accessToken from response for next steps # 3. Get profile (with token) curl -X GET http://localhost:3000/api/auth/profile \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # 4. Refresh token curl -X POST http://localhost:3000/api/auth/refresh-token \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "YOUR_REFRESH_TOKEN" }' # 5. Change password curl -X PUT http://localhost:3000/api/auth/change-password \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "currentPassword": "Password123", "newPassword": "NewPassword123", "confirmNewPassword": "NewPassword123" }' # 6. Forgot password curl -X POST http://localhost:3000/api/auth/forgot-password \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com"}' # 7. Reset password (use token from forgot password) curl -X POST http://localhost:3000/api/auth/reset-password \ -H "Content-Type: application/json" \ -d '{ "token": "RESET_TOKEN_FROM_EMAIL", "password": "NewPassword456", "confirmPassword": "NewPassword456" }' # 8. Logout curl -X POST http://localhost:3000/api/auth/logout \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Test with Postman Collection:
Create a Postman collection with these requests:
Register → POST
/api/auth/registerLogin → POST
/api/auth/login→ Save token as environment variableGet Profile → GET
/api/auth/profile→ Use token in Authorization headerRefresh Token → POST
/api/auth/refresh-tokenLogout → POST
/api/auth/logout
PART 11: FRONTEND INTEGRATION EXAMPLE
Simple React Login Component:
// Login.jsx import React, { useState } from 'react'; import axios from 'axios'; const Login = () => { const [formData, setFormData] = useState({ email: '', password: '' }); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setError(''); try { const response = await axios.post( 'http://localhost:3000/api/auth/login', formData, { withCredentials: true } // Important for cookies ); // Save token to localStorage (or context/state) localStorage.setItem('accessToken', response.data.tokens.accessToken); localStorage.setItem('refreshToken', response.data.tokens.refreshToken); localStorage.setItem('user', JSON.stringify(response.data.user)); alert('Login successful!'); window.location.href = '/dashboard'; } catch (err) { setError(err.response?.data?.message || 'Login failed'); } finally { setLoading(false); } }; return ( <div className="login-container"> <h2>Login</h2> {error && <div className="error">{error}</div>} <form onSubmit={handleSubmit}> <input type="email" placeholder="Email" value={formData.email} onChange={(e) => setFormData({...formData, email: e.target.value})} required /> <input type="password" placeholder="Password" value={formData.password} onChange={(e) => setFormData({...formData, password: e.target.value})} required /> <button type="submit" disabled={loading}> {loading ? 'Logging in...' : 'Login'} </button> </form> </div> ); }; export default Login;
Axios Interceptor for Auto Token Refresh:
// axiosConfig.js import axios from 'axios'; const api = axios.create({ baseURL: 'http://localhost:3000/api', withCredentials: true }); let isRefreshing = false; let failedQueue = []; const processQueue = (error, token = null) => { failedQueue.forEach(prom => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; api.interceptors.response.use( response => response, async error => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then(token => { originalRequest.headers['Authorization'] = 'Bearer ' + token; return api(originalRequest); }).catch(err => Promise.reject(err)); } originalRequest._retry = true; isRefreshing = true; try { const refreshToken = localStorage.getItem('refreshToken'); const response = await axios.post( 'http://localhost:3000/api/auth/refresh-token', { refreshToken } ); const newAccessToken = response.data.accessToken; localStorage.setItem('accessToken', newAccessToken); api.defaults.headers.common['Authorization'] = 'Bearer ' + newAccessToken; originalRequest.headers['Authorization'] = 'Bearer ' + newAccessToken; processQueue(null, newAccessToken); return api(originalRequest); } catch (refreshError) { processQueue(refreshError, null); localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); // Set token from localStorage on app start const token = localStorage.getItem('accessToken'); if (token) { api.defaults.headers.common['Authorization'] = 'Bearer ' + token; } export default api;
PART 12: SECURITY BEST PRACTICES
Add to your server.js:
const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); // Security middleware app.use(helmet()); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again later.' }); app.use('/api/auth', limiter); // Specific rate limiting for login/register const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 attempts per hour message: 'Too many authentication attempts, please try again later.' }); app.use('/api/auth/login', authLimiter); app.use('/api/auth/register', authLimiter);
PART 13: DEPLOYMENT READY CONFIG
Updated .env.production:
NODE_ENV=production PORT=80 # PostgreSQL (using connection string for cloud databases) DATABASE_URL=postgresql://username:password@host:port/database # JWT Secrets (generate strong ones!) JWT_SECRET=your_strong_production_secret_min_32_chars_long JWT_REFRESH_SECRET=your_strong_production_refresh_secret_min_32_chars JWT_EXPIRES_IN=15m JWT_REFRESH_EXPIRES_IN=7d # Security BCRYPT_SALT_ROUNDS=12 # CORS (your frontend domain) CORS_ORIGIN=https://yourdomain.com # Session COOKIE_SECURE=true COOKIE_SAME_SITE=none
Dockerfile for Deployment:
FROM node:18-alpine WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci --only=production # Copy source code COPY . . # Create non-root user RUN addgroup -g 1001 -S nodejs RUN adduser -S nodejs -u 1001 USER nodejs # Expose port EXPOSE 3000 # Start server CMD ["npm", "start"]
PART 14: COMMON ISSUES & SOLUTIONS
Problem: "JWT secret not defined"
Solution: Check your .env file exists and JWT_SECRET is set
Problem: Cookies not working with frontend
Solution: Configure CORS properly:
app.use(cors({ origin: 'http://localhost:5173', // Your frontend URL credentials: true }));
Problem: "password authentication failed"
Solution:
Check PostgreSQL is running
Verify credentials in
.envTest connection:
psql -U postgres -h localhost
Problem: Token expires too quickly
Solution: Adjust in .env:
JWT_EXPIRES_IN=30m # 30 minutes JWT_REFRESH_EXPIRES_IN=30d # 30 days
PART 15: FINAL PROJECT STRUCTURE
auth-tutorial/ ├── node_modules/ ├── src/ │ ├── config/ │ │ └── database.js │ ├── controllers/ │ │ └── authController.js │ ├── middleware/ │ │ ├── authMiddleware.js │ │ └── validationMiddleware.js │ ├── routes/ │ │ └── authRoutes.js │ ├── utils/ │ │ ├── jwtUtils.js │ │ └── passwordUtils.js │ └── server.js ├── .env ├── .env.example ├── .gitignore ├── database-setup.sql ├── docker-compose.yml (optional) ├── Dockerfile (optional) ├── package.json └── README.md
QUICK START COMMANDS:
# 1. Clone and setup git clone <your-repo> cd auth-tutorial npm install # 2. Set up database psql -U postgres -f database-setup.sql # 3. Configure environment cp .env.example .env # Edit .env with your settings # 4. Start development server npm run dev # 5. Test endpoints curl http://localhost:3000
No comments:
Post a Comment