In modern web applications, relational databases are essential for managing structured and transactional data. PostgreSQL is a powerful, open-source relational database known for its reliability, performance, and ease of use. In this comprehensive tutorial, we’ll build a complete User Management App from scratch using PostgreSQL, Node.js, Express, and React.
1. Introduction and Project Overview
Project Title: User Management App
The User Management App is a full-stack application that demonstrates the complete CRUD (Create, Read, Update, Delete) operations cycle using a modern technology stack. This project serves as an excellent starting point for understanding how frontend, backend, and database layers communicate in a real-world application.
Key Features
➕
Add New User
Create new user records with name and email validation
📋
Fetch All Users
Retrieve and display all users from the database
🎨
Modern UI
Clean, responsive interface built with Tailwind CSS
⚡
Fast Development
Hot module replacement with Vite for instant updates
2. Technology Stack
⚛️
React
Frontend Framework
Component-based UI library for building interactive user interfaces with state management and hooks
🟢
Node.js & Express
Backend Server
JavaScript runtime with Express framework for building RESTful APIs and handling HTTP requests
🐘
PostgreSQL
Database
Open-source relational database with ACID compliance, excellent performance, and reliability
Additional Technologies
| Technology |
Purpose |
Why We Use It |
| Vite |
Build Tool |
Lightning-fast development server with HMR |
| Axios |
HTTP Client |
Promise-based HTTP library for API requests |
| Tailwind CSS |
CSS Framework |
Utility-first CSS for rapid UI development |
| pg (node-postgres) |
PostgreSQL Client |
Node.js library for PostgreSQL database connections |
| Express |
Web Framework |
Minimal and flexible Node.js web application framework |
3. Application Architecture
Three-Tier Architecture
Presentation Layer
React Frontend
(Port 5173)
Application Layer
Node.js + Express
(Port 5000)
Data Layer
PostgreSQL Database
(Port 5432)
Data Flow:
- User interacts with React frontend (submits form or requests data)
- React sends HTTP request to Express backend via Axios
- Express receives request and queries PostgreSQL database
- Database processes query and returns results
- Express sends response back to React
- React updates UI with new data
4. Database Setup with PostgreSQL
Open pgAdmin
pgAdmin is a powerful open-source administration and development platform for PostgreSQL. Launch pgAdmin from your applications menu or start menu.
Create Database
Create a new database named
userdb which will store all our user management data.
CREATE DATABASE userdb;
Alternative Method: You can also right-click on “Databases” in the left sidebar of pgAdmin and select “Create > Database” to create the database through the GUI.
Create Users Table
Now we’ll create a table to store user information. This table will have an auto-incrementing ID, name, and email fields.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
Table Schema Breakdown
| Column |
Data Type |
Description |
id |
SERIAL PRIMARY KEY |
Auto-incrementing integer, unique identifier for each user |
name |
VARCHAR(100) |
User’s full name, maximum 100 characters |
email |
VARCHAR(100) |
User’s email address, maximum 100 characters |
Understanding SERIAL: The SERIAL data type in PostgreSQL automatically creates a sequence and sets the column default to the next value from that sequence. This ensures each new user gets a unique, auto-incremented ID.
5. Project Structure Setup
Step-by-Step Project Initialization
Create Root Folder
Create a main folder for the entire project called
User Management App and open it with Visual Studio Code.
mkdir “User Management App” && cd “User Management App”
Setup Frontend with Vite
Step 1: Initialize Vite Project
npm create vite@latest
Step 2: Configure Project
- Project name:
frontend
- Select framework: React
- Select variant: JavaScript
- Use rolldown-vite (Experimental)? No
- Install with npm and start now? Yes
Success: Frontend React application has been created successfully with Vite!
Setup Backend
Step 1: Create Backend Folder
In the root project directory, create a folder named
backend.
mkdir backend
Step 2: Initialize Node.js Project
cd backend
npm init -y
What is npm init -y? This command creates a package.json file with default values. The -y flag accepts all defaults automatically.
Final Project Structure
User Management App/
├── frontend/
│ ├── node_modules/
│ ├── public/
│ ├── src/
│ │ ├── App.jsx
│ │ ├── App.css
│ │ ├── index.css
│ │ ├── main.jsx
│ │ ├── UserForm.jsx
│ │ └── UserList.jsx
│ ├── index.html
│ ├── package.json
│ └── vite.config.js
│
└── backend/
├── node_modules/
├── index.js
├── config.js
└── package.json
6. Backend Setup with Node.js & Express
Create Entry File
Create a file named
index.js in the backend folder. This will be our main server file.
console.log('Hello');
Test the Backend
cd backend
node index.js
Expected Output: You should see “Hello” printed in the terminal.
Install Required Packages
Install the necessary npm packages for our backend server.
npm install pg express
Package Descriptions
| Package |
Purpose |
pg |
PostgreSQL client for Node.js – handles database connections and queries |
express |
Web application framework – simplifies routing and middleware management |
Create Database Configuration
Create a file named
config.js to store database connection settings.
const { Pool } = require('pg');
const pool = new Pool({
user: 'postgres',
host: 'localhost',
database: 'userdb',
password: 'your_password_here',
port: 5432,
});
module.exports = pool;
Important: Replace 'your_password_here' with your actual PostgreSQL password. Never commit sensitive credentials to version control. Consider using environment variables in production.
Configuration Breakdown
- user: PostgreSQL username (default is ‘postgres’)
- host: Database server location (localhost for local development)
- database: Name of the database we created (‘userdb’)
- password: PostgreSQL password you set during installation
- port: PostgreSQL default port (5432)
Create Express Server with API Endpoints
const express = require('express');
const pool = require('./config');
const app = express();
const PORT = 5000;
app.use(express.json());
app.use(require('cors')());
app.get('/users', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM users');
res.json(result.rows);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Database error' });
}
});
app.post('/users', async (req, res) => {
const { name, email } = req.body;
try {
const result = await pool.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[name, email]
);
res.status(201).json(result.rows[0]);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Database error' });
}
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Code Explanation
| Section |
Purpose |
express.json() |
Middleware to parse JSON request bodies |
cors() |
Enables Cross-Origin Resource Sharing for frontend communication |
GET /users |
Endpoint to retrieve all users from database |
POST /users |
Endpoint to create new user in database |
RETURNING * |
PostgreSQL feature that returns the inserted row |
Security Note: The $1, $2 syntax in the query is using parameterized queries, which protects against SQL injection attacks.
Install CORS Package
Since we’re using CORS in the code above, we need to install it:
npm install cors
Start the Backend Server
node index.js
Expected Output: Server running on http://localhost:5000
Your backend is now ready and listening for requests!
7. Frontend Setup with React & Vite
Install Frontend Dependencies
Navigate to the frontend folder and install required packages.
cd frontend
npm install axios tailwindcss @tailwindcss/vite
Package Descriptions
| Package |
Purpose |
axios |
Promise-based HTTP client for making API requests to backend |
tailwindcss |
Utility-first CSS framework for styling |
@tailwindcss/vite |
Tailwind CSS plugin for Vite |
Configure Tailwind CSS with Vite
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
Setup Tailwind CSS
Remove all existing CSS from
App.css and
index.css, then add Tailwind directives.
@import "tailwindcss";
@import "tailwindcss";
Update App.jsx
import React from "react";
import UserForm from "./UserForm";
import UserList from "./UserList";
import "./App.css";
function App() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold text-center mb-8 text-gray-800">
User Management System
</h1>
<div className="grid md:grid-cols-2 gap-8">
<UserForm />
<UserList />
</div>
</div>
</div>
);
}
export default App;
8. Building React Components
Create UserForm Component
This component handles user creation with form validation and API integration.
import React, { useState } from "react";
import axios from "axios";
const UserForm = () => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
if (!name || !email) {
alert("Please fill all fields");
return;
}
try {
await axios.post("http://localhost:5000/users", {
name,
email,
});
alert("User created successfully");
setName("");
setEmail("");
} catch (error) {
console.error(error);
alert("Error creating user");
}
};
return (
<div className="min-h-screen flex items-center justify-center">
<div className="bg-white p-6 rounded-lg shadow-md w-full max-w-md">
<h1 className="text-2xl font-bold text-center mb-6">
Create New User
</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-1">
Enter Name
</label>
<input
id="name"
type="text"
placeholder="Enter Name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
Enter Email
</label>
<input
id="email"
type="email"
placeholder="Enter Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 transition"
>
Submit
</button>
</form>
</div>
</div>
);
};
export default UserForm;
Component Features
- State Management: Uses React hooks (useState) for form fields
- Form Validation: Checks if fields are filled before submission
- API Integration: Posts data to backend using Axios
- User Feedback: Shows success/error alerts
- Form Reset: Clears fields after successful submission
Create UserList Component
This component fetches and displays all users from the database.
import React, { useEffect, useState } from "react";
import axios from "axios";
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
const res = await axios.get("http://localhost:5000/users");
setUsers(res.data);
} catch (error) {
console.error(error);
alert("Failed to fetch users");
} finally {
setLoading(false);
}
};
if (loading) {
return (
<p className="text-center mt-10 text-gray-600">
Loading users...
</p>
);
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow">
<h2 className="text-2xl font-bold mb-4 text-center">
User List
</h2>
{users.length === 0 ? (
<p className="text-center text-gray-500">
No users found
</p>
) : (
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-200">
<th className="border p-2 text-left">ID</th>
<th className="border p-2 text-left">Name</th>
<th className="border p-2 text-left">Email</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id} className="hover:bg-gray-100">
<td className="border p-2">{user.id}</td>
<td className="border p-2">{user.name}</td>
<td className="border p-2">{user.email}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
};
export default UserList;
Component Features
- Data Fetching: Uses useEffect to fetch users on component mount
- Loading State: Shows loading message while data is being fetched
- Empty State: Displays appropriate message when no users exist
- Data Display: Renders users in a clean, responsive table
- Error Handling: Catches and displays fetch errors
9. Testing and Final Output
Start Backend Server
Open a terminal in the backend folder and run:
node index.js
Backend running at: http://localhost:5000
Start Frontend Development Server
Open another terminal in the frontend folder and run:
npm run dev
Frontend running at: http://localhost:5173
Final Application Features
- Users can be added via React form with real-time validation
- Data is securely stored in PostgreSQL database
- User list is fetched from backend and displayed in a responsive table
- Clean, modern UI built with Tailwind CSS
- Real-time updates between form submission and user list
- Error handling for all API requests
- Cross-Origin Resource Sharing (CORS) enabled for secure communication
Testing the Application
Test 1: Add a User
- Open http://localhost:5173 in your browser
- Fill in the “Create New User” form with name and email
- Click “Submit”
- You should see a success alert
- Refresh the page to see the new user in the User List
Test 2: Verify Database
Open pgAdmin and run this query to verify data storage:
SELECT * FROM users;
You should see all users you created through the React form.
10. Best Practices and Optimization
Security Enhancements
Production Considerations:
- Use environment variables for database credentials
- Implement input validation on both frontend and backend
- Add email uniqueness constraint in database
- Implement rate limiting to prevent abuse
- Use HTTPS in production
- Sanitize user inputs to prevent XSS attacks
Performance Optimization
Recommended Improvements:
- Connection Pooling: Already implemented with pg Pool
- Add Indexes: Create indexes on frequently queried columns (email)
- Pagination: Implement pagination for large user lists
- Caching: Use Redis for frequently accessed data
- Debouncing: Add debouncing to form inputs for better UX
Code Quality Improvements
| Improvement |
Implementation |
Benefit |
| Environment Variables |
Use dotenv package for config |
Secure credential management |
| Error Boundaries |
Add React error boundaries |
Graceful error handling in UI |
| TypeScript |
Migrate to TypeScript |
Type safety and better IDE support |
| Testing |
Add Jest and React Testing Library |
Ensure code reliability |
| API Versioning |
Use /api/v1/ prefix |
Future-proof API changes |
Database Schema Enhancement
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
Congratulations! You’ve successfully built a full-stack User Management Application with PostgreSQL, Node.js, Express, and React. This project demonstrates fundamental concepts of modern web development including database design, RESTful API creation, and reactive frontend development.
Next Steps
- Add update and delete functionality (PUT and DELETE endpoints)
- Implement user authentication with JWT
- Add search and filtering capabilities
- Deploy to production (consider platforms like Heroku, Vercel, or AWS)
- Add unit and integration tests
- Implement real-time updates with WebSockets