Comprehensive Guide to Building a Products and Services Website
Step-by-Step Instructions for Creating a Robust Full-Stack Application
Key Takeaways
- Structured Database Design: Implement a normalized PostgreSQL database with clear relationships among users, products, services, categories, and orders.
- Secure and Scalable Backend: Utilize Node.js with ES6, incorporating JWT for authentication, bcrypt for password hashing, and best practices for scalability.
- Dynamic Frontend Integration: Leverage Vue 3 to create a responsive and interactive user interface with efficient routing and state management.
1. Database Design (PostgreSQL)
1.1. Entities and Relationships
Designing a comprehensive and normalized database schema is crucial for the efficient functioning of your website. The primary entities include:
- Users: Manages user information for authentication and tracking orders.
- Categories: Represents main categories for both products and services.
- Subcategories: Subdivisions under each main category to organize products and services further.
- Products: Contains details like descriptions, prices, and photos of products.
- Services: Includes descriptions and showcases project examples with titles, descriptions, and photos.
- Project Examples: Specific instances of services rendered, complete with comments.
- Orders: Tracks user orders for both products and services.
- Comments: User-generated feedback on project examples.
1.2. Database Schema
The following SQL scripts outline the creation of necessary tables with appropriate relationships and constraints:
Users Table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
role VARCHAR(50) DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Categories Table
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT
);
Subcategories Table
CREATE TABLE subcategories (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES categories(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
description TEXT
);
Products Table
CREATE TABLE products (
id SERIAL PRIMARY KEY,
subcategory_id INTEGER REFERENCES subcategories(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
stock INTEGER NOT NULL,
photo_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Services Table
CREATE TABLE services (
id SERIAL PRIMARY KEY,
subcategory_id INTEGER REFERENCES subcategories(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Project Examples Table
CREATE TABLE project_examples (
id SERIAL PRIMARY KEY,
service_id INTEGER REFERENCES services(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT,
image_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Comments Table
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES project_examples(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Orders Table
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
service_id INTEGER REFERENCES services(id) ON DELETE SET NULL,
quantity INTEGER DEFAULT 1,
total_price DECIMAL(10,2) NOT NULL,
status VARCHAR(50) CHECK (status IN ('pending', 'processing', 'completed', 'cancelled')) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Order Items Table
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(id),
service_id INTEGER REFERENCES services(id),
quantity INTEGER NOT NULL,
price DECIMAL(10,2) NOT NULL
);
1.3. Database Optimization
To ensure efficient data retrieval and maintain performance as your application scales, implement indexing on frequently queried columns such as user_id, product_id, and service_id. Additionally, consider using views for complex queries and leveraging PostgreSQL’s built-in full-text search for enhanced search capabilities.
2. Backend Development (Node.js)
2.1. Setting Up the Project
Initialize your Node.js project using ES6 modules and install necessary dependencies:
npm init -y
npm install express pg dotenv bcrypt jsonwebtoken cors
2.2. Project Structure
Organize your backend project with a clear and maintainable folder structure:
backend/
├── config/
│ └── db.js
├── controllers/
│ ├── authController.js
│ ├── productController.js
│ ├── serviceController.js
│ └── orderController.js
├── middleware/
│ └── authMiddleware.js
├── models/
│ ├── User.js
│ ├── Product.js
│ ├── Service.js
│ └── Order.js
├── routes/
│ ├── authRoutes.js
│ ├── productRoutes.js
│ ├── serviceRoutes.js
│ └── orderRoutes.js
├── utils/
│ └── db.js
├── .env
└── index.js
2.3. Database Configuration
Configure the PostgreSQL connection using environment variables for security:
import pkg from 'pg';
const { Pool } = pkg;
import dotenv from 'dotenv';
dotenv.config();
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
});
pool.on('connect', () => {
console.log('Connected to the database.');
});
export default pool;
2.4. User Authentication
Implement secure user authentication using bcrypt for password hashing and JWT for token-based authentication.
Registration Handler
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import pool from '../config/db.js';
export const registerUser = async (req, res) => {
const { username, email, password, firstName, lastName } = req.body;
try {
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = await pool.query(
'INSERT INTO users (username, email, password_hash, first_name, last_name) VALUES ($1, $2, $3, $4, $5) RETURNING *',
[username, email, hashedPassword, firstName, lastName]
);
const token = jwt.sign({ id: newUser.rows[0].id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.status(201).json({ token });
} catch (error) {
res.status(500).json({ error: 'User registration failed.' });
}
};
Login Handler
export const loginUser = async (req, res) => {
const { email, password } = req.body;
try {
const user = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (user.rows.length === 0) {
return res.status(400).json({ error: 'User not found.' });
}
const validPassword = await bcrypt.compare(password, user.rows[0].password_hash);
if (!validPassword) {
return res.status(400).json({ error: 'Invalid password.' });
}
const token = jwt.sign({ id: user.rows[0].id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
} catch (error) {
res.status(500).json({ error: 'Login failed.' });
}
};
Authentication Middleware
import jwt from 'jsonwebtoken';
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
};
2.5. Controllers and Routes
Define controllers to handle business logic and routes to manage API endpoints.
Authentication Routes
import express from 'express';
import { registerUser, loginUser } from '../controllers/authController.js';
const router = express.Router();
router.post('/register', registerUser);
router.post('/login', loginUser);
export default router;
Product Routes
import express from 'express';
import { getProducts, getProductById, createProduct } from '../controllers/productController.js';
import { authenticateToken } from '../middleware/authMiddleware.js';
const router = express.Router();
router.get('/', getProducts);
router.get('/:id', getProductById);
router.post('/', authenticateToken, createProduct);
export default router;
Service Routes
import express from 'express';
import { getServices, getServiceById, createService } from '../controllers/serviceController.js';
import { authenticateToken } from '../middleware/authMiddleware.js';
const router = express.Router();
router.get('/', getServices);
router.get('/:id', getServiceById);
router.post('/', authenticateToken, createService);
export default router;
Order Routes
import express from 'express';
import { createOrder, getOrders } from '../controllers/orderController.js';
import { authenticateToken } from '../middleware/authMiddleware.js';
const router = express.Router();
router.post('/', authenticateToken, createOrder);
router.get('/', authenticateToken, getOrders);
export default router;
2.6. Server Initialization
Set up the Express server to listen for incoming requests:
import express from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import authRoutes from './routes/authRoutes.js';
import productRoutes from './routes/productRoutes.js';
import serviceRoutes from './routes/serviceRoutes.js';
import orderRoutes from './routes/orderRoutes.js';
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/products', productRoutes);
app.use('/api/services', serviceRoutes);
app.use('/api/orders', orderRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
2.7. Security Best Practices
- Password Hashing: Use bcrypt to securely hash user passwords before storing them in the database.
- JWT Authentication: Implement JSON Web Tokens (JWT) to manage user sessions securely.
- Environment Variables: Store sensitive information like database credentials and JWT secrets in environment variables.
- Input Validation: Validate and sanitize all user inputs to prevent SQL injection and cross-site scripting (XSS).
- CORS Configuration: Configure Cross-Origin Resource Sharing (CORS) to restrict access to your API.
- Rate Limiting: Implement rate limiting to protect against brute-force attacks.
2.8. Scalability Considerations
- Database Indexing: Index frequently queried columns to speed up data retrieval.
- Connection Pooling: Use connection pooling to manage database connections efficiently.
- Load Balancing: Distribute incoming traffic across multiple servers to ensure reliability and performance.
- Caching: Implement caching strategies using tools like Redis to reduce database load.
- CDN Usage: Serve static assets through a Content Delivery Network (CDN) to improve load times globally.
3. Frontend Development (Vue 3)
3.1. Project Setup
Initialize your Vue 3 project and install necessary dependencies:
npm install -g @vue/cli
vue create frontend
cd frontend
npm install vue-router@4 axios
3.2. Project Structure
Organize your frontend project with a clear folder structure:
frontend/
├── public/
├── src/
│ ├── assets/
│ ├── components/
│ │ ├── Navbar.vue
│ │ └── Footer.vue
│ ├── pages/
│ │ ├── Home.vue
│ │ ├── Products.vue
│ │ ├── Services.vue
│ │ ├── Login.vue
│ │ ├── Register.vue
│ │ └── Cart.vue
│ ├── router/
│ │ └── index.js
│ ├── store/
│ │ └── index.js
│ ├── App.vue
│ └── main.js
├── .env
└── package.json
3.3. Routing Configuration
Set up Vue Router to manage page navigation:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../pages/Home.vue';
import Products from '../pages/Products.vue';
import Services from '../pages/Services.vue';
import Login from '../pages/Login.vue';
import Register from '../pages/Register.vue';
import Cart from '../pages/Cart.vue';
import store from '../store/index.js';
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/products', name: 'Products', component: Products },
{ path: '/services', name: 'Services', component: Services },
{ path: '/login', name: 'Login', component: Login },
{ path: '/register', name: 'Register', component: Register },
{ path: '/cart', name: 'Cart', component: Cart, meta: { requiresAuth: true } },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// Navigation Guard
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.isAuthenticated) {
next('/login');
} else {
next();
}
});
export default router;
3.4. State Management
Use Vuex or Pinia for managing global state. Below is an example using Vuex:
// src/store/index.js
import { createStore } from 'vuex';
import axios from 'axios';
export default createStore({
state: {
token: localStorage.getItem('token') || '',
user: {},
isAuthenticated: !!localStorage.getItem('token'),
products: [],
services: [],
cart: [],
},
mutations: {
SET_TOKEN(state, token) {
state.token = token;
state.isAuthenticated = true;
},
SET_USER(state, user) {
state.user = user;
},
SET_PRODUCTS(state, products) {
state.products = products;
},
SET_SERVICES(state, services) {
state.services = services;
},
ADD_TO_CART(state, item) {
state.cart.push(item);
},
REMOVE_FROM_CART(state, index) {
state.cart.splice(index, 1);
},
CLEAR_CART(state) {
state.cart = [];
},
},
actions: {
async login({ commit }, credentials) {
const response = await axios.post('/api/auth/login', credentials);
commit('SET_TOKEN', response.data.token);
localStorage.setItem('token', response.data.token);
},
async fetchProducts({ commit }) {
const response = await axios.get('/api/products');
commit('SET_PRODUCTS', response.data);
},
async fetchServices({ commit }) {
const response = await axios.get('/api/services');
commit('SET_SERVICES', response.data);
},
addToCart({ commit }, item) {
commit('ADD_TO_CART', item);
},
removeFromCart({ commit }, index) {
commit('REMOVE_FROM_CART', index);
},
clearCart({ commit }) {
commit('CLEAR_CART');
},
},
modules: {},
});
3.5. Components and Views
Create reusable components and views to display products, services, and handle user interactions.
Products Page
<template>
<div>
<h1>Products</h1>
<div v-for="product in products" :key="product.id" class="product-item">
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
<p>Price: ${{ product.price }}</p>
<img :src="product.photo_url" alt="Product Image" />
<button @click="addToCart(product)">Add to Cart</button>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState(['products']),
},
methods: {
...mapActions(['addToCart']),
},
created() {
this.$store.dispatch('fetchProducts');
},
};
</script>
<style scoped>
.product-item {
border: 1px solid #ccc;
padding: 16px;
margin: 16px;
}
</style>
Services Page
<template>
<div>
<h1>Services</h1>
<div v-for="service in services" :key="service.id" class="service-item">
<h2>{{ service.name }}</h2>
<p>{{ service.description }}</p>
<p>Price: ${{ service.price }}</p>
<button @click="requestService(service)">Request Service</button>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState(['services']),
},
methods: {
...mapActions(['addToCart']),
requestService(service) {
this.addToCart({ ...service, type: 'service' });
},
},
created() {
this.$store.dispatch('fetchServices');
},
};
</script>
<style scoped>
.service-item {
border: 1px solid #ccc;
padding: 16px;
margin: 16px;
}
</style>
Authentication Pages
// Login.vue
<template>
<div>
<h1>Login</h1>
<form @submit.prevent="login">
<input type="email" v-model="email" placeholder="Email" required />
<input type="password" v-model="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
</div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
data() {
return {
email: '',
password: '',
};
},
methods: {
...mapActions(['login']),
async login() {
await this.login({ email: this.email, password: this.password });
this.$router.push('/');
},
},
};
</script>
3.6. API Integration with Axios
Configure Axios to handle HTTP requests to the backend API:
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import axios from 'axios';
axios.defaults.baseURL = process.env.VUE_APP_API_URL || 'http://localhost:5000/api';
if (store.state.token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${store.state.token}`;
}
createApp(App)
.use(store)
.use(router)
.mount('#app');
3.7. UI and UX Enhancements
Enhance the user interface with responsive design, intuitive navigation, and interactive elements. Utilize CSS frameworks like Bootstrap or Tailwind CSS to speed up development and ensure consistency.
3.8. Handling Orders
Implement order management features, allowing authenticated users to place orders for products and services:
// Cart.vue
<template>
<div>
<h1>Your Cart</h1>
<div v-for="(item, index) in cart" :key="index" class="cart-item">
<h2>{{ item.name }}</h2>
<p>Price: ${{ item.price }}</p>
<p>Quantity: {{ item.quantity || 1 }}</p>
<button @click="removeFromCart(index)">Remove</button>
</div>
<button @click="checkout">Checkout</button>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import axios from 'axios';
export default {
computed: {
...mapState(['cart']),
},
methods: {
...mapActions(['removeFromCart', 'clearCart']),
async checkout() {
try {
const response = await axios.post('/orders', {
items: this.cart,
totalPrice: this.cart.reduce((acc, item) => acc + item.price * (item.quantity || 1), 0),
});
alert('Order placed successfully!');
this.clearCart();
this.$router.push('/');
} catch (error) {
alert('Failed to place order.');
}
},
},
};
</script>
4. Security and Scalability
4.1. Security Measures
- Password Security: Utilize bcrypt for hashing passwords before storing them in the database.
- Authentication: Implement JWT for secure user authentication and session management.
- Environment Variables: Store sensitive information like database credentials and JWT secrets in environment variables using the dotenv package.
- Input Validation: Validate and sanitize all user inputs to prevent vulnerabilities like SQL injection and XSS.
- CORS: Configure CORS to control which domains can interact with your API.
- Rate Limiting: Implement rate limiting to protect against brute-force attacks and abuse of API endpoints.
- HTTPS Enforcement: Ensure that all data transmission between the client and server is encrypted using HTTPS.
- Security Headers: Use security headers like Content Security Policy (CSP) to add an additional layer of security.
4.2. Scalability Strategies
- Database Optimization: Use indexing on frequently accessed columns to speed up queries.
- Connection Pooling: Manage database connections efficiently using connection pooling to handle multiple simultaneous requests.
- Load Balancing: Distribute incoming traffic across multiple servers to ensure reliability and performance.
- Caching: Implement caching mechanisms using Redis or similar tools to reduce database load and improve response times.
- Content Delivery Network (CDN): Serve static assets through a CDN to improve load times and performance for users globally.
- Microservices Architecture: Consider breaking down the application into smaller, manageable services to enhance scalability and maintainability.
- Horizontal Scaling: Add more instances of your application to handle increased load efficiently.
- Database Replication and Sharding: Use replication to improve database availability and sharding to distribute data across multiple machines.
5. Deployment
5.1. Backend Deployment
- Cloud Providers: Deploy the Node.js backend to cloud platforms like Heroku, AWS Elastic Beanstalk, or DigitalOcean.
- Environment Configuration: Use environment variables to manage sensitive data and configurations securely.
- CI/CD Pipelines: Set up Continuous Integration and Continuous Deployment pipelines using tools like GitHub Actions or Jenkins to automate testing and deployment.
5.2. Frontend Deployment
- Hosting Services: Deploy the Vue 3 frontend using platforms like Netlify, Vercel, or GitHub Pages.
- Build Optimization: Optimize your build for performance by minimizing bundle sizes and leveraging lazy loading where appropriate.
- SSL Certificates: Ensure your frontend is served over HTTPS by configuring SSL certificates, often handled automatically by hosting providers.
5.3. Database Deployment
-
Managed Services: Use managed PostgreSQL services like Amazon RDS, Heroku Postgres, or Supabase for scalability, backups, and maintenance.
-
Backup Strategies: Implement regular backups and set up automatic failover to ensure data integrity and availability.
-
Monitoring: Use monitoring tools to keep track of database performance and receive alerts for any issues.
6. Conclusion
Building a comprehensive website that offers both products and services involves meticulous planning and execution across multiple layers of technology. By designing a robust database schema with PostgreSQL, developing a secure and scalable backend with Node.js, and creating a dynamic and responsive frontend with Vue 3, you can deliver a seamless user experience. Implementing best practices for security and scalability ensures that your application remains reliable and efficient as it grows.
References
Last updated January 20, 2025