What this chapter is really for
Split at slide 30: Part 1 (1–29) is MVC, database theory and basic Sequelize CRUD; Part 2 (30–46) is richer queries, associations and validators.
Notice the shape of the chapter. It opens with a problem — many API endpoints in one file becomes difficult to maintain and debug — and its answer is MVC, which is presented as an application of separation of concerns, called on slide 4 “probably the most important principle in software engineering”. That is the same principle CSS was justified with back in chapter 3. It is a very likely exam question precisely because it ties the whole course together.
The conceptual half
MVC and its three parts. SQL vs NoSQL, and what data integrity and data consistency actually mean. Key-value stores vs document stores. Why an ORM is worth using. The CRUD-to-HTTP-verb table. All short slides, all easy marks if learned.
The code half
Configure, connect, define a model, sync. Then five route handlers, the Op
operators, associations, and validators. Every handler has the same shape — learn that shape
once and the rest is filling in a blank.
router.METHOD(path, async (req, res) => { → try { →
await Model.someQuery({...}) → res.status(n).json(result) →
} catch (error) { res.json(error) }. If you can type that skeleton blind, every code
question in the chapter reduces to knowing which query and which options object.All 47 slides, weighted
| Slides | Topic | Weight | What to do with it |
|---|---|---|---|
| 1–2 | Title; technical notes | Skim | Use a local MySQL server, or the free online service at aiven.io. You need this for the slide 46 task. |
| 3–5 | The complex server problem; MVC | Memorize | Highest-value conceptual slide in the chapter. Learn all three roles and the separation-of-concerns justification. |
| 6–7 | The role of databases; DBMS options | Memorize | The design principle: separate static content from dynamic content. Appearance (HTML/CSS) is static; data is dynamic. |
| 8–9 | NoSQL; why and why not | Memorize | NoSQL = Not-only-SQL. Learn the definitions of data integrity and data consistency word for word — they are given as formal definitions. |
| 10–11 | Key-value stores; document stores | Memorize | Key-value stores are analogous to Maps. A document store calls the value a document — a binary file, or semi-structured XML or JSON. |
| 12–14 | MongoDB; relational vs document data | Memorize | Data goes in as JSON and is stored as BSON. Know what MongoDB does not support: transactions (before 4.0) and joins — it uses nested documents instead. |
| 15–16 | How websites use databases; ORM libraries | Memorize | Three ORM advantages: fewer injection risks, simpler database communication, and the ability to change database system without changing your code. |
| 17–19 | Sequelize; configuring and connecting | Write it | npm install sequelize mysql2 — you need a driver as well as the ORM. Learn the four constructor arguments. |
| 20 | Creating the data model | Write it | The densest code slide in Part 1. Note that a model called Employee produces a table called employees, and what sync({alter:true}) does. |
| 21 | CRUD and HTTP methods | Memorize | The four-row table. Guaranteed to appear in some form, and it reaches back to chapter 1. |
| 22 | Designing the API routes | Memorize | Note which routes return web pages and which return JSON, and the modularity argument for the shared /api/employees/v1/ prefix. |
| 23–24 | Creating a record; testing the endpoint | Write it | build() then save(). Status 201 means "resource created" — not 200. |
| 25–28 | findAll, findOne, criteria, projections | Write it | A projection means selecting which properties you want back — because some properties should stay secret. |
| 29–31 | VS Code extensions; connecting from VS Code | Skim | Practical setup for the class task. MySQL and Thunder Client extensions. |
| 32–33 | Generating mock data | Skim | Useful for testing, but read it — the mock config shows the full Employee schema including city, age and the timestamps. |
| 34–38 | WHERE clauses: AND, OR, delete, not equal, gt/lt | Write it | All five use the Op object, which must be imported. Learn the operator names. |
| 39 | Bulk create at table creation | Write it | The count() guard is the interesting part — only seed if the table is empty. |
| 40–41 | Model associations | Write it | hasMany + belongsTo. Know which side gets the foreign key and what it is named. |
| 42–45 | Validation vs constraints; the validator list | Memorize | The distinction on slide 42 is the examinable one. Validations run in JavaScript before any SQL is sent; constraints are enforced by the database. |
| 46 | Class task | Write it | Do it end to end. It is the practical exam in miniature. |
MVC, SQL vs NoSQL, and why an ORM
MVC — learn all three roles
The problem it solves, stated on slide 3: when you have many API endpoints you end up with a complex file that becomes difficult to maintain and debug.
| Part | Responsibility | In this course |
|---|---|---|
| Model | The parts of the application that store the data (the database) and manipulate it — save, update, delete, retrieve. | Your Sequelize models and queries. |
| View | The code responsible for presenting data to the user — web pages, mobile interface, GUI. | Your EJS templates from chapter 6, and the HTML/CSS from chapters 2–3. |
| Controller | Links model and view. Selects the view that will present the data, selects the model operation to execute, and returns the results to the view. | The Express router plus your business logic — the slides say so explicitly. |
Why databases at all
Databases implement an important software design principle: separate static content from dynamic content. On the web, the visual appearance — the HTML and CSS — is static, while the data content changes.
SQL vs NoSQL
NoSQL stands for Not-only-SQL: a category of database software that does not use the relational table model. These systems rely on a different set of ideas for data modelling that put fast retrieval ahead of other considerations like consistency.
| Relational (SQL) | NoSQL | |
|---|---|---|
| Examples | SQLite, MySQL, PostgreSQL, Oracle Database, IBM DB2, Microsoft SQL Server. | Cassandra, Firebase, MongoDB, DynamoDB. |
| Model | Tables with a schema. | Key-value stores, document stores and others. |
| Strength | Schemas ensure data integrity and data consistency. | Handles huge datasets better than relational systems. |
| Trade-off | Less comfortable with very large datasets. | Not the best answer for all scenarios — you give up the guarantees a schema provides. |
Two definitions to learn verbatim
Data integrity: the guarantee of all data constraints — primary and foreign keys, data types, and so on.
Data consistency: the guarantee that database constraints are not violated when executing transactions.
Two NoSQL families
| Key-value store | Document store |
|---|---|
| Every value — integer, string, or other data structure — has an associated key. Analogous to Maps. | Also associates keys with values, but calls the value a document. |
| Allows fast retrieval through means such as a hash function, so there is no need for indexes on multiple fields as there is in SQL. | A document can be a binary file such as a .doc or .pdf, or a semi-structured XML or JSON document. |
| Examples: DBM, Berkeley DB. | Most NoSQL systems are of this type. MongoDB, AWS DynamoDB, Google Firebase, Cloud Datastore. |
MongoDB specifically
- Open-source, NoSQL, document-oriented. Usable with any backend, but much more commonly used with Node.
- You package data as a JSON object and MongoDB stores it as a binary JavaScript object — BSON.
- It does not support transactions (before version 4.0) and does not support joins — it uses nested documents instead.
- Running on multiple servers means it handles large datasets: replication gives redundancy and high availability.
ORM libraries
An Object Relational Mapping library lets you use an API to query databases without using database-specific queries, giving a layer of abstraction that makes your code independent of the database system underneath.
| ORM advantage | What it means in practice |
|---|---|
| Reduces security risks | SQL and non-SQL injection are much harder when you never concatenate a query string yourself. |
| Simplifies database communication | You write Employee.findAll() rather than SQL plus connection plumbing. |
| Allows changing the database system without changing your code | Swap the dialect, keep the queries. |
Sequelize supports MySQL, DB2, SQL Server and more. It needs a database driver as well — the API Sequelize uses to reach a particular system. The stack is: Express server app → Sequelize → DB driver for MySQL → MySQL DB server.
npm install sequelize mysql2 # the ORM AND the driver — both are neededConfigure, model, and the five CRUD handlers
Configure and connect
// 1 — import the sequelize library const Sequelize = require('sequelize'); // 2 — create a configured Sequelize object with our connection data const sequelize = new Sequelize( 'se371db', // database name 'se371', // database user 'se371pwd', // user password { dialect: 'mysql', // which database server — change this to switch DBMS host: 'localhost' // a local db, so localhost } ); // 3 — connect. authenticate() is async, so await it inside try/catch. const connectToDB = async () => { try { await sequelize.authenticate(); console.log(`Successfully connected to database server...`); } catch (error) { console.log(error); } }; module.exports = { sequelize, connectToDB };
The data model
const db = require("../config/database"); // the connection object const { DataTypes } = require('sequelize'); // the type definitions const Employee = db.sequelize.define('Employee', { id: { type: DataTypes.INTEGER, primaryKey: true }, // ↑ NOTE: a model named 'Employee' creates a table named 'employees' // — Sequelize pluralises and lower-cases it automatically. name: { type: DataTypes.STRING, allowNull: false, // a CONSTRAINT — enforced by the database validate: { max: 100 } // a VALIDATION — enforced by Sequelize }, position: { type: DataTypes.STRING, defaultValue: "Developer" } }); // Apply the model to the actual database. // table missing → it is CREATED // table exists → changes are APPLIED to its structure db.sequelize.sync({ alter: true }); module.exports = Employee;
CRUD → HTTP verb → Express function
| Database operation | HTTP method | Why | Express function |
|---|---|---|---|
| Create | POST | Used to create new data in the backend. | app.post() |
| Read | GET | The default — used to get a resource from the server: a webpage, CSS file, image, data. | app.get() |
| Update | PUT | Used to update existing data in the backend. | app.put() |
| Delete | DELETE | Used to delete data. | app.delete() |
get or post. PUT and DELETE have to be sent from JavaScript — which is exactly what fetch from chapter 6 is for. The two facts are one question waiting to be asked together.Designing the routes
| Operation | Route | Method | Returns |
|---|---|---|---|
| Open the home page | / | GET | A web page |
| Open the form page | /employees/ | GET | A web page |
| Retrieve | /api/employees/v1//api/employees/v1/id/:id/api/employees/v1/position/:position | GET | JSON |
| Create | /api/employees/v1/ (using a form)/api/employees/v1/id/:id/name/:name/position/:position | POST | JSON |
| Update | /api/employees/v1/id/:id/name/:name/position/:position | PUT | JSON |
| Delete | /api/employees/v1/id/:id | DELETE | JSON |
The reason for the shared /api/employees/v1/ prefix is given on the slide:
you can create a router that handles only employee-related operations, which gives better
modularity in your code. The v1 is API versioning — it lets you ship a
v2 later without breaking existing clients.
The five handlers
app.post('/api/employees/v1/', async (request, response) => { const { id, name, position } = request.body; // destructuring (ch. 4) const newEmployee = Employee.build({ "id": id, "name": name, "position": position }); try { await newEmployee.save(); response.status(201).json(newEmployee); // 201 = RESOURCE CREATED, not 200 } catch (error) { response.json(error); } }); // build() makes the object in memory; save() writes it to the database. // Employee.create({...}) does both in one call. // // Testing it (slide 24): change the method to POST, set the URL, and send // the test data as a JSON body. The 201 response comes back with the // record plus metadata Sequelize added (createdAt, updatedAt).
// ── SELECT ALL ───────────────────────────────────────────────── router.get('/api/employees/v1/', async (request, response) => { const employees = await Employee.findAll(); response.status(200).json({ employees: employees }); // a JSON ARRAY }); // ── SEARCH BY ID — findOne returns ONE object ────────────────── router.get('/employees/v1/:id', async (request, response) => { try { const employee = await Employee.findOne({ where: { id: request.params.id } // req.params — chapter 6 }); response.status(200).json(employee); } catch (error) { response.json(error); } }); // ── SEARCH BY CRITERIA — findAll returns an ARRAY ────────────── router.get('/employees/v1/position/:position', async (request, response) => { try { const employee = await Employee.findAll({ where: { position: request.params.position } }); response.status(200).json(employee); } catch (error) { response.json(error); } }); // ── PROJECTION — choose WHICH properties come back ───────────── // "some properties should stay secret" router.get('/employees/v1/position/:position', async (request, response) => { try { const employee = await Employee.findAll({ attributes: ['id', 'name'], // ← the projection where: { position: request.params.position } }); response.status(200).json(employee); } catch (error) { response.json(error); } });
Operators, associations and validators — Part 2
The Op object — richer WHERE clauses
const { Op } = require('sequelize'); // MUST be imported — it defines the operations // ── AND — slide 34 ───────────────────────────────────────────── const employee = await Employee.findAll({ where: { [Op.and]: [{ city: request.params.city }, { position: request.params.position }] } }); // the [Op.and] square brackets are a COMPUTED PROPERTY NAME — Op.and // is a symbol, not the literal text "Op.and". // ── OR — slide 35 ────────────────────────────────────────────── await Employee.findAll({ where: { [Op.or]: [{ city: request.params.city }, { position: request.params.position }] } }); // ── NOT EQUAL — slide 37. Note it nests INSIDE the column. ───── await Employee.findAll({ where: { city: { [Op.ne]: request.params.city } } // ne = not equal }); // ── LESS THAN — slide 38 ─────────────────────────────────────── await Employee.findAll({ where: { age: { [Op.lt]: request.params.age } } // lt / gt / lte / gte }); // ── DELETE, with a WHERE — slide 36 ──────────────────────────── router.delete('/employees/v1/city/:city', async (request, response) => { try { const employee = await Employee.destroy({ where: { city: request.params.city } }); response.status(200).json(employee); // returns the NUMBER of rows deleted } catch (error) { response.json(error); } });
Where the operator goes
Combining conditions (Op.and, Op.or) — the
operator is the outer key and takes an array of conditions.
Comparing one column (Op.ne, Op.lt, Op.gt)
— the operator goes inside that column’s object. Getting this backwards is the
most common Sequelize error in the chapter.
Seeding data at creation
db.sequelize.sync({ alter: true })
.then(async () => {
Country.count() // how many records are there?
.then(async (count) => {
if (!count) { // 0 is FALSY (chapter 4) → table empty
await Country.bulkCreate([
{ name: "KSA" },
{ name: "Oman" },
{ name: "Egypt" }
]);
}
});
});
// The count() guard is the point: re-run this whenever tables are
// deleted and recreated, without duplicating the seed data.Model associations
const Employee = db.sequelize.define('Employee', { /* … */ }); // Every employee is associated with exactly one country const Country = db.sequelize.define('Country', { name: { type: DataTypes.STRING, unique: true } }, { timestamps: false // no createdAt/updatedAt — static, non-critical data }); // ── DECLARE THE RELATIONSHIP FROM BOTH SIDES ─────────────────── Country.hasMany(Employee); // one country → many employees Employee.belongsTo(Country); // each employee → one country // CONSEQUENCES: // • Employee gets the FOREIGN KEY, named CountryId (Model + Id) // • Employee instances gain a getter: .getCountry() // ── USING IT — slide 41 ──────────────────────────────────────── router.get('/employees/v1/:id', async (request, response) => { try { const employee = await Employee.findOne({ where: { id: request.params.id } }); const country = await employee.getCountry(); // ← the enriched API response.status(200).json({ employee, country }); } catch (error) { response.json(error); } });
belongsTo. One country has many employees, so the key lives on Employee and is called CountryId. Compare this with MongoDB, which has no joins at all and nests the document instead.Validations vs constraints — the distinction to learn
| Validation | Constraint | |
|---|---|---|
| Where | At the Sequelize level, in pure JavaScript. | At SQL level — rules defined in the database. |
| On failure | No SQL query is sent to the database at all. | An error is thrown by the database. |
| Written as | A validate: { … } block. | Field options such as allowNull: false and unique: true. |
const User = sequelize.define('user', { username: { type: DataTypes.STRING, allowNull: false, // ← CONSTRAINT: not null, at SQL level unique: true, // ← CONSTRAINT: unique, at SQL level }, hashedPassword: { type: DataTypes.STRING(64), validate: { is: /^[0-9a-f]{64}$/i, // ← VALIDATION: enforced by Sequelize }, }, }); // The regex is chapter 5 material: ^ start, $ end, {64} exactly 64, // [0-9a-f] hex characters, /i case-insensitive. // ── Using a validator — slide 45 ─────────────────────────────── const User = sequelize.define('user', { email: { type: DataTypes.STRING, validate: { isEmail: true } }, }); // Then in the controller: const user = User.build({ username: 'Salah', email: 'salah' }); if (user.validate()) { try { await user.save(); } catch (error) { /* … */ } }
The validator list — slides 43–44
| Group | Validators |
|---|---|
| Pattern | is: /^[a-z]+$/i matches this RegExp · not: /^[a-z]+$/i does not match it |
| Format | isEmail · isUrl · isIP (IPv4 or IPv6) · isDate |
| Character type | isAlpha letters only · isAlphanumeric (so "_abc" fails) · isLowercase · isUppercase |
| Numeric | isNumeric · isInt · isFloat · isDecimal |
| Emptiness | notNull · isNull only allows null · notEmpty no empty strings |
| Value | equals: 'specific value' · contains: 'foo' · notContains: 'bar' · isIn: [['foo','bar']] · notIn: [['foo','bar']] |
| Range | len: [2,10] · min: 23 allows values ≥ 23 · max: 23 allows values ≤ 23 · isAfter: "2011-11-05" · isBefore: "2011-11-05" |
Map these back onto the six validation types from chapter 2 slide 66 — required, correct data type, correct format, comparison, range check, custom. Every one of them has a Sequelize validator, and that is the full-circle answer to "where should validation happen".
The seven things people get wrong
Slide 23 sends response.status(201) and slide 24 spells out why: 201 means "resource created". Returning 200 is not an error the code will catch, but it is wrong and it is marked.
Fix: 201 for a successful create, 200 for a successful read or update. Chapter 1’s status-code families are being tested again here.
Op.ne in the wrong placewhere: { [Op.ne]: { city: x } } does not work. Column-comparison operators nest inside the column, while Op.and and Op.or sit outside and take an array.
Fix: Combining conditions → operator outside, array inside. Comparing a column → column outside, operator inside: where: { city: { [Op.ne]: x } }.
OpSlide 34 opens with the import for a reason. Without const { Op } = require('sequelize') the symbol is undefined and the where clause silently does not filter the way you meant.
Fix: Import it at the top of any file that uses an operator.
Both prevent bad data, but the exam asks where the check happens. A validation fails in JavaScript and no SQL query is sent at all; a constraint fails at the database, which throws the error back.
Fix: Field-level options (allowNull, unique) are constraints. Anything inside validate: {} is a validation.
With Country.hasMany(Employee) and Employee.belongsTo(Country), people look for an employees column on Country. It is not there.
Fix: The foreign key goes on the many side — on Employee, named CountryId. The convention is model name plus Id.
The route is app.delete(...), the form says method="delete", and the browser quietly sends a GET instead. Chapter 2 slide 43 already warned that forms accept only GET and POST.
Fix: Send PUT and DELETE with fetch from JavaScript, or test them with a client such as Thunder Client.
findOne and findAll the wrong way roundfindAll always returns an array, even when it matches exactly one row — so result.name is undefined. findOne returns the object itself, or null.
Fix: Searching by primary key → findOne. Searching by any other criteria → findAll, then index into the array.
Chapter 7 on a single screen
MVC
- Model — stores and manipulates data
- View — presents data to the user
- Controller — links the two (Express router + logic)
- An application of separation of concerns
SQL vs NoSQL
- NoSQL = Not-only-SQL
- Puts fast retrieval ahead of consistency
- Schemas give integrity + consistency
- Key-value store ≈ a Map
- Document store — most NoSQL systems
MongoDB
- Document-oriented, JSON in, BSON stored
- No transactions (before 4.0)
- No joins — uses nested documents
- Replication → redundancy + high availability
CRUD
- Create →
POST→app.post() - Read →
GET→app.get() - Update →
PUT→app.put() - Delete →
DELETE→app.delete() - Forms can only send GET and POST
Sequelize setup
npm install sequelize mysql2new Sequelize(db, user, pwd, {dialect, host})await sequelize.authenticate()db.sequelize.define('Employee', {…})sync({ alter: true })Employeemodel →employeestable
Queries
Model.build({})then.save()Model.findAll()→ arrayModel.findOne({where})→ object or nullModel.destroy({where})→ rows deletedattributes: ['id','name']— projectionModel.bulkCreate([…])
Operators
const { Op } = require('sequelize')[Op.and]: [ {}, {} ]— outside[Op.or]: [ {}, {} ]— outsidecol: { [Op.ne]: v }— inside[Op.lt][Op.gt][Op.lte][Op.gte]
Associations
Country.hasMany(Employee)Employee.belongsTo(Country)- Foreign key on the MANY side:
CountryId - Gives
employee.getCountry()
Validation vs constraint
- Validation — JavaScript, no SQL sent
- Constraint — SQL level, DB throws
allowNullunique→ constraintsvalidate: { isEmail, len, min, max, is }
Do the class task, then extend it
Slide 46 sets the task and it is worth treating as the practical exam: create an account on
aiven.io, create se371db, install the MySQL and Thunder Client extensions,
connect, and exercise GET, POST, DELETE and PUT. Everything below builds on that.
- Write the MVC definition from memory, then label which files in your chapter 6 project are model, view and controller.
- Define data integrity and data consistency in one sentence each, without looking.
- Complete the class task end to end: aiven account, database, extensions, connection, and all four verbs tested.
- Write the Sequelize configuration and connection code from memory, including the try/catch.
- Define the
Employeemodel with an id primary key, a non-null name with a max length, and a position with a default value. - Run
sync({alter: true}), then add a field to the model and run it again — watch the table structure change. - Generate a hundred mock rows using the slide 32 configuration, including
cityandage. - Write all four CRUD handlers and confirm the create route returns 201, not 200.
- Add a projection so one route returns only
idandname, and explain in a comment why you would want that. - Write an AND query and an OR query, then swap where the operator sits and read the error.
- Write a not-equal query and a less-than query — note that these operators go inside the column.
- Write a delete-by-criteria route and check what the return value actually is.
- Add a
Countrymodel with a unique name and no timestamps, associate it both ways, and find the foreign key column in the employees table. - Seed three countries with
bulkCreate, guarded bycount(), then drop the table and restart to confirm the guard works. - Add one validation and one constraint to the same model, break each in turn, and compare the two error messages.
- Take the six validation types from chapter 2 slide 66 and write a Sequelize validator for each one.
- Finally: wire an EJS page from chapter 6 to display
findAll()results in a table. That is the whole course in one file.
There is no chapter-7 example folder in your study material, but two solved labs cover this ground:
lab 10 and
lab 11.
The full code repository for every chapter is at github.com/skanderturki/se371.