Lecture Notes: Web Application Architecture
Dr. Hassan Eldeeb & Eng. Mostafa Abo Elnaga | Tanta University
Course: Internet Technologies | Date: Feb, 2025
Introduction to Databases
Overview
Databases are essential for storing, retrieving, and managing data in a structured manner.
They are used in applications ranging from small-scale personal projects to enterprise-level
systems.
Types of Databases
● Relational Databases (SQL): Data is stored in structured tables with predefined
schemas.
● NoSQL Databases: Flexible schema design, often used for unstructured or
semi-structured data.
Relational Databases
Key Concepts
● Relational Model: Data is stored in tables with rows and columns.
● Relationships:
○ One-to-One
○ One-to-Many
○ Many-to-Many
● Schema Enforcement:
○ Defined data types
○ Constraints (e.g., NOT NULL, UNIQUE)
● SQL (Structured Query Language):
○ Data Definition Language (DDL): Creating and modifying schema.
○ Data Manipulation Language (DML): Querying and updating records.
Benefits
● ACID Transactions: Ensures data integrity.
○ Atomicity: Ensures all or nothing execution.
○ Consistency: Data remains valid before and after transactions.
○ Isolation: Transactions are independent of each other.
○ Durability: Ensures committed transactions persist.
● Ideal for structured data and complex queries.
When to Use Relational Databases
● Applications requiring strict consistency (e.g., financial systems).
● Scenarios involving complex joins and structured relationships.
Example: Creating a MySQL Table
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
NoSQL Databases
Key Concepts
● Document-Oriented Storage: Data is stored as JSON-like documents.
● Dynamic Schema: Fields can vary across documents.
● Horizontal Scaling: Easily distributes data across multiple servers.
Benefits
● Flexible schema evolution.
● Efficient handling of nested objects.
When to Use NoSQL Databases
● Quick prototyping.
● Handling large-scale applications with unstructured or semi-structured data.
Example: MongoDB Document
{
"_id": "object_id_here",
"name": "Alice",
"email": "
[email protected]",
"interests": ["reading", "gaming"],
"createdAt": "2025-02-17T12:34:56Z"
}
ORM vs. Query Builders
Object-Relational Mapping (ORM)
● Maps database tables to objects in code.
● Popular ORMs:
○ Sequelize (PostgreSQL, MySQL, SQLite, MSSQL)
○ Prisma (TypeScript-first approach, strong type safety)
○ Mongoose (MongoDB ODM, schema definitions, and validation)
Pros
● Reduces repetitive SQL.
● Enforces consistent schema.
Cons
● Overhead for complex queries.
● Learning curve.
Query Builders
● Constructs SQL queries programmatically.
● Example: Knex.js for Node.js:
const knex = require('knex')({
client: 'pg',
connection: {
host: '127.0.0.1',
user: 'postgres',
password: 'password',
database: 'mydb'
}
});
knex('users').select('*').then(rows => console.log(rows));
Pros
● More control over queries.
● Less overhead.
Cons
● Fewer high-level abstractions.
Designing a Database Schema
Steps
1. Requirements Gathering:
○ Identify entities (User, Product, Post, Comment).
○ Define relationships (1:N, N:N).
2. Choosing SQL or NoSQL:
○ SQL: Normalized structure, strict schema.
○ NoSQL: Denormalized, flexible schema.
Example: Relational Schema for a Blog
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
user_id INT REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Example: NoSQL Schema (MongoDB)
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: String,
email: { type: String, unique: true },
createdAt: { type: Date, default: Date.now }
});
const postSchema = new mongoose.Schema({
title: String,
content: String,
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
createdAt: { type: Date, default: Date.now }
});
Implementing CRUD APIs
Express + Sequelize (PostgreSQL)
Install Dependencies
npm install express sequelize pg pg-hstore
Database Setup
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('mydb', 'postgres', 'password', {
host: 'localhost',
dialect: 'postgres'
});
User Model
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const User = sequelize.define('User', {
name: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, allowNull: false, unique: true }
});
CRUD Operations
const express = require('express');
const app = express();
const User = require('./models/User');
app.use(express.json());
app.post('/api/users', async (req, res) => {
try {
const newUser = await User.create(req.body);
res.status(201).json(newUser);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.get('/api/users', async (req, res) => {
const users = await User.findAll();
res.json(users);
});
app.listen(3000, () => console.log('Server running at http://localhost:3000'));
Express + Mongoose (MongoDB)
Install Dependencies
npm install express mongoose
MongoDB Setup
const mongoose = require('mongoose');
const connectDB = async () => {
await mongoose.connect('mongodb://localhost:27017/mydb');
};
CRUD Operations
const express = require('express');
const User = require('./models/User');
const app = express();
app.use(express.json());
connectDB();
app.post('/api/users', async (req, res) => {
const newUser = await User.create(req.body);
res.status(201).json(newUser);
});
app.get('/api/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
app.listen(3000, () => console.log('Server running at http://localhost:3000'));
A detailed example is presented in the
Appendix
Summary
● SQL: Structured, ACID compliance.
● NoSQL: Flexible, scalable.
● ORM vs. Query Builder trade-offs.
● Designing schemas effectively.
● Implementing CRUD APIs with PostgreSQL & MongoDB.
Contact:
● Email: [email protected]
● Email: [email protected]
Appendix:
Example system overview: E-Commerce
Consider a simple e-commerce application called “ShopSimple” where users can browse
products, add them to an order, and make a purchase.
● Users can register and log in.
● Products can be categorized.
● Orders are created by users and contain multiple order items (each referencing a
product).
High-level requirements
1. User management
○ Each user has a unique email and password (stored securely, e.g., hashed).
○ a user can have many orders.
2. Catalog management
○ products each have a name, description, price.
○ products belong to one or more categories (optional complexity: a product
might be in multiple categories, or keep it simple with a one-to-one
relationship?).
3. Order processing
○ An Order references a user and has a status (e.g., pending, completed,
canceled).
○ Order Items (line items) reference both the Order and a Product, including a
quantity.
4. Checkout Flow (high-level)
○ Payment and shipping details can be stored, but to keep it simple we’ll
skip/ignore advanced payment workflows.
Entity-Relationship Diagram (ERD)
Below is an ERD for ShopSimple (using a relational approach).
Link: here (via Mermaid Live Editor)
Relationships:
users : orders = 1 : N
orders : order_items = 1 : N
products : order_items = 1 : N
products : categories = M : N
○ Achieved via a junction table (e.g., product_categories(product_id,
category_id)).
Schema definitions (PostgreSQL + Sequelize)
Below is an example of how you might define these tables in a relational context. We’ll use
Sequelize to keep the code consistent with the Node.js environment.
Install & initialize
```bash
npm install express sequelize pg pg-hstore
```
```js
// db.js (Sequelize connection)
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('shopsimple_db', 'postgres',
'password', {
host: 'localhost',
dialect: 'postgres',
});
module.exports = sequelize;
```
Models: Users, Products, Categories
```js
// models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const User = sequelize.define('User', {
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
});
module.exports = User;
```
```js
// models/Product.js
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const Product = sequelize.define('Product', {
name: {
type: DataTypes.STRING,
allowNull: false,
},
price: {
type: DataTypes.DECIMAL(10, 2), // e.g. 99999999.99
allowNull: false,
},
});
module.exports = Product;
```
```js
// models/Category.js
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const Category = sequelize.define('Category', {
name: {
type: DataTypes.STRING,
allowNull: false,
},
});
module.exports = Category;
```
Models: Orders & OrderItems
```js
// models/Order.js
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const Order = sequelize.define('Order', {
status: {
type: DataTypes.ENUM('pending', 'completed',
'canceled'),
allowNull: false,
defaultValue: 'pending',
},
});
module.exports = Order;
```
```js
// models/OrderItem.js
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const OrderItem = sequelize.define('OrderItem', {
quantity: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
},
});
module.exports = OrderItem;
```
Model associations
In Sequelize, you define relationships after all models are initialized. Typically, you can place
this in an associations.js file or near the model definitions.
```js
// models/index.js (or associations.js)
const User = require('./User');
const Order = require('./Order');
const OrderItem = require('./OrderItem');
const Product = require('./Product');
const Category = require('./Category');
const sequelize = require('../db');
// 1) A User has many Orders
User.hasMany(Order, { foreignKey: 'user_id' });
Order.belongsTo(User, { foreignKey: 'user_id' });
// 2) An Order has many OrderItems
Order.hasMany(OrderItem, { foreignKey: 'order_id' });
OrderItem.belongsTo(Order, { foreignKey: 'order_id' });
// 3) A Product has many OrderItems
Product.hasMany(OrderItem, { foreignKey: 'product_id' });
OrderItem.belongsTo(Product, { foreignKey: 'product_id' });
// 4) Products <-> Categories (M:N)
Product.belongsToMany(Category, {
through: 'product_categories',
foreignKey: 'product_id',
otherKey: 'category_id',
});
Category.belongsToMany(Product, {
through: 'product_categories',
foreignKey: 'category_id',
otherKey: 'product_id',
});
// Sync all
const initDB = async () => {
try {
await sequelize.sync({ alter: true }); // 'alter'
for dev only, not recommended in production
console.log('All models were synchronized
successfully.');
} catch (error) {
console.error('DB sync error:', error);
}
};
module.exports = { initDB, User, Order, OrderItem, Product,
Category };
Considerations & Concerns
● ACID & transactions
- In an e-commerce setting, you must ensure an order is created with correct line
items.
- Consider using Sequelize transactions for operations that must all succeed or fail
together.
● Indexing
- For frequently searched fields (like product name), add indexes to speed up queries.
● Scaling
- If read/write volume grows, consider load balancers, read replicas, or microservices
splitting inventory management, user management, etc.
● Data consistency
- With many concurrent orders, ensure product availability is updated properly to
avoid overselling (beyond scope here, but relevant in real e-commerce).
● Security
- Passwords must be hashed (e.g., using bcrypt).
- Use environment variables to store DB credentials, never commit them to source
control.
Sample code: setting up CRUD endpoints
Below is a brief example showing a few routes for User and Product.
```js
// index.js
const express = require('express');
const { initDB, User, Product } = require('./models');
const app = express();
app.use(express.json());
// Initialize DB (sync models)
initDB();
// Create a new user
app.post('/api/users', async (req, res) => {
try {
// real app: hash password here with bcrypt
const newUser = await User.create(req.body);
res.status(201).json(newUser);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Get all users
app.get('/api/users', async (req, res) => {
try {
const users = await User.findAll();
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message
});
}
});
// Create a new product
app.post('/api/products', async (req, res) => {
try {
const newProduct = await Product.create(req.body);
res.status(201).json(newProduct);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Get all products
app.get('/api/products', async (req, res) => {
try {
const products = await Product.findAll();
res.json(products);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(4000, () => {
console.log('ShopSimple backend running at
http://localhost:4000');
});
```
Notes:
● In a real production environment, we would add authentication, authorization,
more advanced error handling, and possibly pagination on product listings.
● For Orders and OrderItems, we would also create their CRUD operations /
endpoints for “Add to Cart”, “Checkout”, and so on.