Skip to content
Chapter 07 · Slide Breakdown

Working with Databases

The last chapter, and the one that turns your Express routes into a real application. Half of it is conceptual and very examinable — MVC, SQL vs NoSQL, CRUD verbs — and half is Sequelize code you will be asked to write.

47 slidesTwo decks in oneHalf concept, half codeBook ch. 15

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.

Every Sequelize route in this chapter is the same five lines: 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

SlidesTopicWeightWhat to do with it
1–2Title; technical notesSkimUse a local MySQL server, or the free online service at aiven.io. You need this for the slide 46 task.
3–5The complex server problem; MVCMemorizeHighest-value conceptual slide in the chapter. Learn all three roles and the separation-of-concerns justification.
6–7The role of databases; DBMS optionsMemorizeThe design principle: separate static content from dynamic content. Appearance (HTML/CSS) is static; data is dynamic.
8–9NoSQL; why and why notMemorizeNoSQL = Not-only-SQL. Learn the definitions of data integrity and data consistency word for word — they are given as formal definitions.
10–11Key-value stores; document storesMemorizeKey-value stores are analogous to Maps. A document store calls the value a document — a binary file, or semi-structured XML or JSON.
12–14MongoDB; relational vs document dataMemorizeData 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–16How websites use databases; ORM librariesMemorizeThree ORM advantages: fewer injection risks, simpler database communication, and the ability to change database system without changing your code.
17–19Sequelize; configuring and connectingWrite itnpm install sequelize mysql2 — you need a driver as well as the ORM. Learn the four constructor arguments.
20Creating the data modelWrite itThe densest code slide in Part 1. Note that a model called Employee produces a table called employees, and what sync({alter:true}) does.
21CRUD and HTTP methodsMemorizeThe four-row table. Guaranteed to appear in some form, and it reaches back to chapter 1.
22Designing the API routesMemorizeNote which routes return web pages and which return JSON, and the modularity argument for the shared /api/employees/v1/ prefix.
23–24Creating a record; testing the endpointWrite itbuild() then save(). Status 201 means "resource created" — not 200.
25–28findAll, findOne, criteria, projectionsWrite itA projection means selecting which properties you want back — because some properties should stay secret.
29–31VS Code extensions; connecting from VS CodeSkimPractical setup for the class task. MySQL and Thunder Client extensions.
32–33Generating mock dataSkimUseful for testing, but read it — the mock config shows the full Employee schema including city, age and the timestamps.
34–38WHERE clauses: AND, OR, delete, not equal, gt/ltWrite itAll five use the Op object, which must be imported. Learn the operator names.
39Bulk create at table creationWrite itThe count() guard is the interesting part — only seed if the table is empty.
40–41Model associationsWrite ithasMany + belongsTo. Know which side gets the foreign key and what it is named.
42–45Validation vs constraints; the validator listMemorizeThe distinction on slide 42 is the examinable one. Validations run in JavaScript before any SQL is sent; constraints are enforced by the database.
46Class taskWrite itDo 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.

PartResponsibilityIn this course
ModelThe parts of the application that store the data (the database) and manipulate it — save, update, delete, retrieve.Your Sequelize models and queries.
ViewThe 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.
ControllerLinks 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.
The sentence to write in an exam: MVC is an application of separation of concerns, probably the most important principle in software engineering. Its advantages: the code is more maintainable, and it is easier to provide different views of the same data — adding a mobile app to an existing web app, for instance. That is the same principle used to justify CSS in chapter 3.

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
ExamplesSQLite, MySQL, PostgreSQL, Oracle Database, IBM DB2, Microsoft SQL Server.Cassandra, Firebase, MongoDB, DynamoDB.
ModelTables with a schema.Key-value stores, document stores and others.
StrengthSchemas ensure data integrity and data consistency.Handles huge datasets better than relational systems.
Trade-offLess 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 storeDocument 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 advantageWhat it means in practice
Reduces security risksSQL and non-SQL injection are much harder when you never concatenate a query string yourself.
Simplifies database communicationYou write Employee.findAll() rather than SQL plus connection plumbing.
Allows changing the database system without changing your codeSwap 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.

terminalSlide 17
npm install sequelize mysql2      # the ORM AND the driver — both are needed

Configure, model, and the five CRUD handlers

Configure and connect

config/database.jsSlides 18–19
// 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

models/employee.jsSlide 20
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 operationHTTP methodWhyExpress function
CreatePOSTUsed to create new data in the backend.app.post()
ReadGETThe default — used to get a resource from the server: a webpage, CSS file, image, data.app.get()
UpdatePUTUsed to update existing data in the backend.app.put()
DeleteDELETEUsed to delete data.app.delete()
Remember chapter 2 slide 43: an HTML form can only use 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

OperationRouteMethodReturns
Open the home page/GETA web page
Open the form page/employees/GETA web page
Retrieve/api/employees/v1/
/api/employees/v1/id/:id
/api/employees/v1/position/:position
GETJSON
Create/api/employees/v1/ (using a form)
/api/employees/v1/id/:id/name/:name/position/:position
POSTJSON
Update/api/employees/v1/id/:id/name/:name/position/:positionPUTJSON
Delete/api/employees/v1/id/:idDELETEJSON

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

CREATE — slide 23Slides 23–24
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).
READ — slides 25–28Slides 25–28
// ── 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

operators.jsSlides 34–38
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

bulk-create.jsSlide 39
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

associations.jsSlides 40–41
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);
    }
});
Which side gets the foreign key? The many side — the one that 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

ValidationConstraint
WhereAt the Sequelize level, in pure JavaScript.At SQL level — rules defined in the database.
On failureNo SQL query is sent to the database at all.An error is thrown by the database.
Written asA validate: { … } block.Field options such as allowNull: false and unique: true.
validation-vs-constraint.jsSlides 42, 45
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

GroupValidators
Patternis: /^[a-z]+$/i matches this RegExp · not: /^[a-z]+$/i does not match it
FormatisEmail · isUrl · isIP (IPv4 or IPv6) · isDate
Character typeisAlpha letters only · isAlphanumeric (so "_abc" fails) · isLowercase · isUppercase
NumericisNumeric · isInt · isFloat · isDecimal
EmptinessnotNull · isNull only allows null · notEmpty no empty strings
Valueequals: 'specific value' · contains: 'foo' · notContains: 'bar' · isIn: [['foo','bar']] · notIn: [['foo','bar']]
Rangelen: [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

Returning 200 for a created resource

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.

Putting Op.ne in the wrong place

where: { [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 } }.

Forgetting to import Op

Slide 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.

Confusing a validation with a constraint

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.

Expecting the foreign key on the wrong table

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.

Trying to send PUT or DELETE from an HTML form

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.

Getting findOne and findAll the wrong way round

findAll 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 → POSTapp.post()
  • Read → GETapp.get()
  • Update → PUTapp.put()
  • Delete → DELETEapp.delete()
  • Forms can only send GET and POST

Sequelize setup

  • npm install sequelize mysql2
  • new Sequelize(db, user, pwd, {dialect, host})
  • await sequelize.authenticate()
  • db.sequelize.define('Employee', {…})
  • sync({ alter: true })
  • Employee model → employees table

Queries

  • Model.build({}) then .save()
  • Model.findAll() → array
  • Model.findOne({where}) → object or null
  • Model.destroy({where}) → rows deleted
  • attributes: ['id','name'] — projection
  • Model.bulkCreate([…])

Operators

  • const { Op } = require('sequelize')
  • [Op.and]: [ {}, {} ] — outside
  • [Op.or]: [ {}, {} ] — outside
  • col: { [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
  • allowNull unique → constraints
  • validate: { 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.

  1. Write the MVC definition from memory, then label which files in your chapter 6 project are model, view and controller.
  2. Define data integrity and data consistency in one sentence each, without looking.
  3. Complete the class task end to end: aiven account, database, extensions, connection, and all four verbs tested.
  4. Write the Sequelize configuration and connection code from memory, including the try/catch.
  5. Define the Employee model with an id primary key, a non-null name with a max length, and a position with a default value.
  6. Run sync({alter: true}), then add a field to the model and run it again — watch the table structure change.
  7. Generate a hundred mock rows using the slide 32 configuration, including city and age.
  8. Write all four CRUD handlers and confirm the create route returns 201, not 200.
  9. Add a projection so one route returns only id and name, and explain in a comment why you would want that.
  10. Write an AND query and an OR query, then swap where the operator sits and read the error.
  11. Write a not-equal query and a less-than query — note that these operators go inside the column.
  12. Write a delete-by-criteria route and check what the return value actually is.
  13. Add a Country model with a unique name and no timestamps, associate it both ways, and find the foreign key column in the employees table.
  14. Seed three countries with bulkCreate, guarded by count(), then drop the table and restart to confirm the guard works.
  15. Add one validation and one constraint to the same model, break each in turn, and compare the two error messages.
  16. Take the six validation types from chapter 2 slide 66 and write a Sequelize validator for each one.
  17. 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.

Can you answer these without scrolling up?

Question 1