@ George Madeley @ Personal Studies @ 3/15/24
This is a collection of notes that I, George Madeley, took when taking the Codecademy Back-End Engineering career path. I do not take ownership of the material covered and these notes should only be used for educational purposes.
Section 2: Designing Relational Databases
3 - How do I make and Populate My own Database?
Section 3: Advanced PostgreSQL
7 - Connecting a Database to a Server
SQL, Structure Query Language, is a programming language designed to manage data stored in relational databases.
A relational database is a database that organizes information into one or more tables. A table is a collection of data organized into rows and columns. Tables are sometimes referred to as relations. A row is a single record in a table.
All data stored in a relational database is of a certain data type. Some of the most common data types are:
-
INTEGER A positive or negative whole number,
-
TEXT A string,
-
DATE The date formatted as YYY-MM-DD,
-
REAL Decimal value.
A statement is text that the database recognises as a valid command. Statements always end in a semi-colon.
CREATE TABLE table_name (
column_1 data_type,
column_2 data_type,
column_3 data_type
);
The code that is in capital letters is a clause. A clause performs specific tasks in SQL.
CREATE statements allow us to create a new table in the database.
CREATE TABLE (
id INTEGER,
name TEXT,
age INTEGER
);
The INSERT statement inserts a new row into the table.
INSERT INTO celebs (id, name, age)
VALUES (1, 'Justin Bieber', 21);
SELECT statements are used to fetch data from a database.
SELECT name FROM celebs;
You can also query all data from all columns.
SELECT * FROM celebs;
The ALTER TABLE statement adds a new column to the table.
ALTER TABLE celebs
ADD COLUMN twitter_handle TEXT;
The UPDATE statement edits a row in a table. You can use the UPDATE statement when you want to change existing records.
UPDATE celebs
SET twitter_handle = '@taylorswift13'
WHERE id = 4;
The DELETE FROM statement deletes one or more rows from a table. You can use the statement when you want to delete existing records.
DELETE FROM celebs
WHERE twitter_handle IS NULL;
Constraints that add information about how a column can be used are invoked after specifying the data type for a column. They can be used to tell the database to reject inserted data that does not adhere to certain restrictions.

Queries allow us to communicate with the database by asking questions and returning a result set with data relevant to the question.
Queries allow us to communicate with the database by asking questions and returning a result set with data relevant to the question.
We can use the SELECT command to state what columns we want.
SELECT column_1, column_2
FROM table_name;
AS is a keyword in SQL that allows you to rename a column or table using an alias.
SELECT name as 'Titles'
FROM movies;
DISTINCT is used to return unique values in the output. It filters out all duplicate values.
SELECT DISTINCT tools
FROM inventory;
We can restrict our query results using the WHERE clause to obtain the information we want.
SELECT *
FROM movies
WHERE imdb_rating > 8;
LIKE can be a useful operator when you want to compare similar values.
SELECT *
FROM movies
WHERE name LIKE "se_en";
% is a wildcard character that matches zero or more missing letters in the pattern. I.e., A% matches with all strings in a specified column starting with an A.
SELECT *
FROM movies
WHERE name LIKE '%man%';
It is not possible to test for NULL values with = or != operators. Instead, we must have to use the following:
-
IS NULL
-
IS NOT NULL
SELECT *
FROM movies
WHERE imdb_rating IS NOT NULL;
The BETWEEN operator is used in a WHERE clause to filter the result set within a certain range.
SELECT *
FROM movies
WHERE year BETWEEN 1990 AND 1999
With AND both conditions must be true for the row to be included in the result.
SELECT *
FROM movies
WHERE year < 1985 AND genre = 'horror';
The OR operator displays a row if any condition is true.
We can sort the result using ORDER BY, either alphabetically or numerically.
SELECT *
FROM movies
ORDER BY name;
-
DESC is a keyword used in order by to sort the results in descending order.
-
ASC is a keyword used in order by to sort the results in ascending order.
SELECT *
FROM movies
WHERE imdb_rating > 8
ORDER BY year DESC;
LIMIT is a clause that lets you specify the maximum number of rows the result set will have.
SELECT *
FROM movies
LIMIT 10;
A CASE statement allows us to create different outputs. It is SQLs way of handling if-then logic.
SELECT name
CASE
WHEN imdb_rating > 8 THEN 'Fantastic'
WHEN imdb_rating > 6 THEN 'Poorly Received'
ELSE 'Avoid at All Costs'
END
FROM movies;
Calculation performed on multiple rows of a table are called aggregates.
The fastest way to calculate how many rows are in a table is to use the COUNT() function.
SELECT COUNT(*)
FROM table_name
SUM() is a function that takes the name of a column as an argument and returns the sum of all the values in that column.
SELECT SUM(*)
FROM table_name;
The MAX() and MIN() functions return the highest and lowest values in a column, respectfully.
SELECT MAX(downloads)
FROM music
The AVG() function works by taking a column name as an argument and returns the average value for that column.
SELECT AVG(downloads)
FROM music
The ROUND() function takes two arguments inside the paranthesis:
-
A column name,
-
An integer.
It rounds the values in the column to the number of decimals places specified by the integer.
SELECT ROUND(price, 0)
FROM music;
GROUP BY is a clause in SQL that is used with aggregate functions. It is used in collaboration with the SELECT statement to arrange identical data into groups.
The GROUP BY statement comes after any WHERE statement, but before ORDER BY or LIMIT.
SELECT price, COUNT(*)
FROM fake_apps
GROUP BY price;
This is great if you wanted to count all downloads for each music category for instance.
SQL let us use column reference(s) in our GROUP BY that will make our lives easier.
-
1 is the first column selected.
-
2 is the second column selected.
-
3 is the third column selected.
SELECT ROUND(imdb_rating), COUNT(name)
FROM movies
GROUP BY 1
ORDER BY 1;
In the above example, 1 corresponds to ROUND(imdb_rating).
SQL allows you to filter which groups to include and which to exclude.
HAVING is the exact same as WHERE but a WHERE filter rows, HAVING filters groups.
SELECT year, genre, COUNT(name)
FROM movies
GROUP BY 1, 2
HAVING COUNT(name) > 10;
HAVING statements always come after GROUP BY, but before ORDER BY and LIMIT.
We often spread related data over multiple tables.
To join two tables together (create a relation) we use the JOIN keyword.
SELECT * FROM orders
JOIN customers
ON orders.customer_id = customers.customer_id;
When we perform a JOIN (often called inner join), our results only include rows that match our ON condition:
A LEFT JOIN will keep all rows from the first table, regardless of whether there is a matching row in the second table.
SELECT * FROM table1
LEFT JOIN table2
ON table1.c2 = table2.c2;
The column that uniquely define each record is called the primary key. They require the following:
-
None of them can be null,
-
Each value must be unique,
-
A table cannot have more than one primary key.
When a primary for one table appears in another table, it is termed the foreign key.
CROSS JOIN allows us to keep all data from all tables.
The resulting table is all data combinations.
SELECT * FROM table1
CROSS JOIN table2;
Sometimes, we just want to stack one dataset on top of another. UNION allows us to do just that.
SQL has some rules through:
-
Tables must have the same number of columns.
-
The columns must have the same data type.
SELECT * FROM table1
UNION
SELECT * FROM table2;
The WITH statement allows us to perform a separate query then apply the result to a new query.
Postman is a GUI that aids in the development of APIs by making it easy to test requests and their responses in an organised way.
Postman is a very useful tool which will be used quite frequently in the debugging stage.
A database is a set of data stored in a computer.
A relational database is a type of database. It uses a structure that allows us to indetigy and access data in relation to another piece of data in the database. A relational database management system (RDBMS) is a program that allows you to create update, and administer a relational database.
NoSQL is a type of database which is non-relational. Data in this type can be stored in a multitude of ways allowing for flexibility and scalability.
Postgre is an opensource RDBMS. It is itself a database "server" and in order to run it on your computer, you will need to set up both a Postgres server and a client. For a client, it is recommended to use PostBird.
Postgre is a popular database management system that stores information on a dedicated database server instead of on a local file system, the benefits of using a database system includes better organisation of related information, more efficient storage, an faster retrieval.
A database schema is documentation that helps its audience such as a database designer, administrator, and other users interact with a database. It gives an overview of the purpose of the database along with the data that makes up the database, how the data is organized into tables, how the tables are internal structured and how they relate to one another.
You can design database schemas by hand or by software:
-
DbDigram.io -- a free, simple to draw ER diagrams by just writing code.
-
SQLDBM -- SQL database modeller,
-
DB Designer -- online database schema design and modelling tool.
A database table is made up of columns of information. Each column is assigned a name and a data type. Top create a table in PostgreSQL, we would use the following SQL syntax:
CREATE TABLE person (
first_name VARCHAR(15),
last_name VARCHAR(15),
age INTEGER,
...
ssn CHAR(9),
);
Each column name is associated with a column type which is a data type such as numeric, character, Boolean, or other interesting types.
Data Type Representation Value Display
INTEGER Whole number 617 617
DECIMAL Floating-point number 26.17345 26.17345
MONEY Fixed floating-point number with two 6.17 $6.17 decimal
BOOLEAN Logic TRUE, FALSE t, f
CHAR(n) Fixed length string removes trailing '123' '123' blanks
VARCHAR(n) Variable-length string '123' '123'
To insert data into a PostgreSQL table, use this syntax:
INSERT INTO table_name VALUES (
column_one_value,
column_two_value,
...
column_n_value
);
To query a table to return all the columns, type:
SELECT * FROM table_name;
A database key is a column or group of columns in a table that uniquely identifies a row in a table.
A primary key is designation that apples to a column or multiple columns of a table that uniquely identify each row in a table.
To designate a primary key in a table, type PRIMARY KEY keyword in all caps next to the selected column when creating a table.
CREATE TABLE recipe (
id INTEGER PRIMARY KEY,
name TEXT,
);
The information schema is a database containing meta information about objects in the database including tables, columns, and constraints.
To determine if a column has been designated correctly as a primary key, we can query a special view, high_column_usage, generated in the current database that are restricted by soe constraint such as a primary or foreign key.
SELECT constraint_name, table_name, column_name
FROM information_schema.key_column_usage
WHERE table_schema = 'recipe';
Sometimes, none of the columns in a table can uniquely identify a record. When this happens, we can designate multiple columns in a table to server as the primary key.
To designate multiple columns as a composite primary key, use this syntax:
PRIMARY KEY (column_one, column_two)
Within CREATE TABLE but as the last statement.
To maintain data integrity and ensure that we can join tables together correctly, we can use another type of key called a foreign key.
A foreign key is a key that refers to a column in another table.
To designate a foreign key on a single column in PostgreSQL, we use the REFERENCES keyword:
CREATE TABLE email (
email varcahr(20) PRIMARY KEY,
person_id integer REFERENCES person(id)
);
To ensure these two tables are correctly joined:
SELECT person.name AS name, email.email AS email
FROM person, email
WHERE person.id = email.person_id;
This is just a basic query but if the returned result is correct, the two tables have successfully been related.
In a one-to-one relationship, a row of table
licence_id char(2) REFERENCES driver(licence_id) UNIQUE
This type of relationship is where one element in one table is related to multiple records in another table. Each person can have multiple emails but an email can only have one owner.
To implement a many-to-many relationship in a relational database, we would create a third cross-reference table also known as a join table. It will have the following constraints:
-
Foreign keys referencing the priary keys of the two member tables,
-
A composite priary key made up of the two foreign keys.
A database trigger is procedural code that is automatically executed in response to certain events on a particular table or view in a database. The trigger is mostly used for maintaining the integrity of the information on the database.

- You may see EXECUTE FUNCTION instead of EXECUTE PROCEDURE.
-
BEFORE -- this calls your trigger before the query that fired the trigger runs.
-
AFTER -- occur once the query finishes its work. This will not let your modify the row that has been updated/inserted.
When using FOR EACH ROW, the trigger will fire and call the function for every row that is impacted by the related query. FOR EACH STATEMENT call the function in the trigger once fr each query, not each record.
You can use a WHEN clause to filter when a trigger calls its related function
CREATE TRIGGER <trigger_name>
BEFORE UPDATE ON <table_name>
FOR EACH ROW
EXECUTE PROCEDURE <function_name>;
You can use NEW or OLD to get records from the table before and after the query.
-
INSERT does not have an OLD.
-
DELETE does not have a NEW
If multiple triggers are triggered, they are executed in alphabetical order. SELECT does not trigger a trigger. If a trigger executes an UPDATE command, any trigger that is triggered by an UPDATE is also called.
To remove a trigger:
CREATE TRIGGER insert_trigger_high
BEFORE INSERT ON Clients
FOR EACH ROW
WHEN (NEW.total_spent >= 1000)
EXECUTE PROCEDURE high_spender();
To find a list of all triggers:
DROP TRIGGER <trigger_name> ON <table_name>;
Constraints are rules defined as apart of the data model to control what values are allowed in specific data columns and tables.
Specifically, constraints:
-
Reject inserts or updates containing values that shouldn't be inserted into a database table, which can help with preserving data integrity and quality.
-
Raise an error when they're violated, which can help with debugging applications that write to the DB.
In a CREATE TABLE statement we can specify the data type for each column of a table:
Name Description
INTEGER Whole number between
BOOLEAN True/false
VARCHAR or VARCHAR(n) Text with variable length , up to n characters
DATE Calendar date
TIME Time of day
However, a lot of type casting errors can occur.
We can choose to reject inserts and updates that don't include data for specific columns by adding a NOT NULL constraints on those columns.
SELECT * FROM information_schema.triggers;
We can use ALTER TABLE statements to add or remove constraints from existing tables.
CREATE TABLE talks (
id INTEGER,
tile VARCHAR NOT NULL,
speaker_id INTEGER NOT NULL,
);
If the column does not already meet the constraint, it will not be applied.
If you use a WHERE statement to find columns equal NULL, use:
ALTER TABLE talks
ALTER COLUMN sesion_timeslot SET NOT NULL;
We can use CHECK statements to implement more precise constraints on our tables. To use a check constraints, we list CHECK(...) following the data type in a CREATE TABLE statement and write the condition we'd like to test for inside he parantheses.
The condition must evaluate to true or false.
WHERE <column_name> IS NULL;
To identify values in a single column as unique, we specify UNIQUE following the column name and datatype definitions.
ALTER TABLE talks
ADD CHECK (estimated_length > 0);
CASCADE clauses cause the updates or deletes to automatically be applied to any child tables.
ALTER TABLE attendees
ADD UNIQUE (email);
This means, if we had an artists and songs table and decided to update/delete a record in artists, this would cause all related records in songs to also be updated or deleted.
When you create a new PostgreSQL database server, there will be a single database and a single user available. You can run the following command to check the name of the current user.
ALTER TABLE <table_name>
ADD FOREIGN KEY (<foreign_id>)
REFERENCES <foreign_table>(<foreign_id>) ON DELETE CASCADE;
The initial user has the ability o create new databases, tables, users, etc. this user is termed superuser.
The following tables and columns are particularly useful for understanding the state of any users permissions:
-
pg_catalog.pg_roles -- a listing of all users in the database and understand what special permissions these users have.
-
Information_schema.table_privileges -- description of the permissions apply to a user on a table.
As a superuser, you can use SET ROLE to mimic permissions of other users.
Roles can either be login roles or group roles. Login roles are used for most routine database activity. Group roles typically do no have the ability to login themselves, but can hold other roles as "members" and allow access to certain shared permissions.
The CREATE ROLE statement takes a series of arguments that modify the specific paramters around the newly-created users permissions.
SELECT current_user;
Some of the most commonly used permissions are described below.
-
SUPERUSER -- is the role of a superuser?
-
CREATEROLE -- is the role permitted to create additional roles?
-
CREATEDB -- Is the role able to create databases?
-
LOGIN -- is the role able to login?
-
IN ROLE -- list of existing roles that a role will be added to as a new member.
As a superuser, table owner, or schema owner, you may use GRANT and REVOKE statements to modify these permissions at the schema and table level.
To use a schema, a role must have a permission called USAGE. Without USAGE, a role cannot access tables within that schema. Other schema level permissions include CREATE and DROP.
CREATE ROLE sampleuser WITH NOSUPERUSER LOGIN;
With default permissions, a superuser can set permissions to be updated automatically when new objects are created in a schema.
GRANT USAGE, CREATE ON SCHEMA finance TO analyst;
GRANT SELECT, UPDATE ON finance.revenue TO analyst;
REVOKE UPDATE ON fianance.revenue FROM analyst;
If a new table is created in a schema, then the role automatically has the stated permission of it.
GRANT USAGE ON finance TO analyst;
GRANT SELECT ON ALL TABLES IN finance TO analyst;
Login users can be apart of groups and in doing so, inherit those groups permissions.
- PostgreSQL disallows the inheritance of certain powerful permissions such as LOGIN, SUPERUSER, CREATEDB, and CREATEROLE.
There are several ways to create a new group role:
ALTER DEFAULT PRIVILEGES IN SCHEMA finance
GRANT SLEECT ON TABLES TO analyst;
You can also add users to group(s) on creations by specifying IN ROLE along with the CREATE ROLE statement.
CREATE ROLE marketing WITH NOLOGIN ROLE alice, bob;
CREATE ROLE finance WITH NOLOGIN;
GRANT finance TO charlie;
Sometimes we'll want more five grained permissions than as the table or schema level.
CREATE ROLE fran WITH LOGIN IN ROLE employees, managers;
There are a few required steps to enable row level security. First, we create a policy using a CREATE POLICY statement.
GRANT SELECT (project_code, project_name, project_status)
ON projects TO employees;
Next, we need to enable RLS on the tale the policy refers to.
CREATE POLICY emp_rls_policy ON accounts FOR SELECT
TO sales USING (salesperson = current_user);
Any single unit of work done to the database is defined as a transaction. To reduce the number of errors that can occur when working with transactions. These transactions must maintain ACID properties.
-
Atomic -- All changes to data are performed as if they are a single operation. That is, all the changes are performed, or none of them are.
-
Consistent -- Data is in a consistent state when a transaction starts and when it sends.
-
Isolation -- the intermediate state of a transaction is invisible to other transactions. As a result, transaction that run concurrently appear to be serialized.
-
Durable -- after a transaction successfully computes, chages to data persist and are not undone, even in the event o a system failure.
A SQL injection is a common vulnerability affecting applications that use SQL as their database language.
Here are some common injections:
-
Union-based injections,
-
Error-based injections,
-
Boolean-based injections,
-
Time-based injections,
-
Out-of-band SQL injections.
There are two main methods for preventing injection attacks.
-
Sanitization -- is the process of removing dangerous characters from user input. We would want to escape dangerous characters such as:
-
'.
-
;.
-
\--,
-
-
Prepared Statements -- We provide the database the query we want to execute in advance. First, the database processes out query. Then we pass in the parameters/user input.
An index is an organisation of the data in a table to help with performance when searching and filtering records. A table can have zero, one, or many indexes.
Let's say you want to see what indexes exist on your products table, you would run the following query:
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;
Indexing allows you to organise your database structure in such a way that it makes finding specific records much faster. This allows for binary search.
EXPLAIN ANALYZE prefix before a query will return information about the query.
SELECT * FROM pg_Indexes WHERE tablename = 'products';
The above query would return the plan that the server will use to give you every row from every record from the 'customers' table.
There ar two things to take note on:
-
'seq scan' and 'index scan' -- this tells you how the query is searching the table.
-
'planning time' and 'executing time' -- this is the time taken for planning then executing the query.
If a column does nt have an index, it will take longer to search.
The CREATE INDEX keywords can be used to create an index on a column of a table.
EXPLAIN ANALYZE SELECT * FROM customers;
You can combine multiple columns together as a single index. The index
is built in the specific order listed at creation, so
CREATE INDEX <index_name> ON <table_name> (<column_name>);
The DROP INDEX command can be used to drop an existing index.
<table_name>_<i1>_<i2>_idx
If you update, insert, or delete a record with an index, the table will need to be reorganized which can become very costly.
- Updating a non-indexed column has no negative impact.
In additional to this, indexes take up a lot of space. If you want to examine the size of a table, you would run:
DROP INDEX IF EXISTS customer_city_idx;
A partial index allows for indexing on a subset of a table allowing searches to be conducted on just this group of records in a table. To create one, you just need to add a where clause:
SELECT pg_size_pretty(pg_total_relation_size('table_name'));
To specify the order of an index, you can add on the order you want your index sorted in when you create the index.
CREATE INDEX users_user_name_internal_idx
ON users(user_name)
WHERE email_address LIKE '%@wellsfargo.com';
If your column contains nulls, the order they appear can also be set by using NULLS FIRST or NULLS LAST. Postgree automatically does nulls last.
Primary keys are also indexes.
All indices are tiher a clustered index or a non-clustered index. A clustered index is often tieed to the primary key. When you modify or add a record, Postgre does not automatically reorder the table. To reorder, you must use the cluster command. To cluster your database table using an existin index:
CREATE INDEX logins_date_time-idx
ON logins(date_time DESC, user_name);
If you're already established a cluster key:
CLUSTER products USING products_product_name_idx;
You can also cluster all tables at once:
CLUSTER products;
A non-clustered index stores only the index in a table and orders only those indexes. Each index has a pointer to the record in the other table. This way, we can order the table in multiple ways without impacting the original table itself.
Normalization is the process of cleaning a database and making it more efficient.
We can create a table from a preexisting table:
CLUSTER;
A 1NF database is an atomic database. An atomic database is when each ceel contains one value, and each row is unique.
When a database is 2NF, it means that the database is 1NF and does not contain any partial dependencies. A partial dependency is when an attribute depends on part of the table's primary key rather than the whole primary key. To remove partial dependencies, we will need to split the table into two or more tables.
A database is described as 3NF when it is 2NF but also has no transitive functional dependencies. A transitive functional dependency is when a non-prime attribute depends on another non-prime attribute rather than a primary key or prime attribute.
You can use the following functions t check the size of a relation in a database.
-
Pg_total_relation_size -- will return the size of the table and all it's indexes in bytes.
-
Pg_table_size and pg_indexes_size -- return the size of the tables data and table's indexes in bytes.
-
Pg_size_pretty -- can be used with the functions about to format a number in bytes, kb, MB, or GB.
CREATE TABLE majors AS
SELECT distinct major_1, major_1_credits_reqd
FROM college;
You can also call pg_total_relation_size on a given index to find the size of that one index.
When an update or delete occurs, the original record does not actually get modified or removed. Instead, it is marked as invalid causing it to not appear in queries but still take up disk space. These records are termed dead tuples.
VACUUM is a command which will clear a tables dead tuples where possible.
SELECT
pg_size_pretty(pg_table_size('time_series'))
AS tbl_size,
pg_size_pretty(pg_indexes_size('time_series'))
AS idx_size,
pg_size_pretty(pg_total_relation_size('time_series'))
AS total_size;
If a table name is not provided, VACUUM will clear the entire database of dead tuples.
Most tables in PostgreSQL have a property called autovacuum. With this property, PostgreSQL regularly checks all tables and runs VACUUM on those which have had a large number of updates or deletions.
You can monitor the last VACUUM by querying the table pg_stat_all_tables for vacuum and analyze statistics.
VACUUM <table_name>;
Where relname is the table name.
We can use the columns n_dead_tup and n_live_tup from this table to asses the status of the table.
SELECT relname, last_vacuum, last_autovacuum, last_analyze
FROM pg_stat_user_tables
WHERE relname = 'books';
VACUUM FULL rewrites all the data from a table into a "new" location on disk and only copies the required data (excluding dead tuples).
This operation is very slow and prevents any commands being performed on the table whilst the vacuum operation is being run.
TRUNCATE quickly removes all rows from a table. It also simultaneously reclaims disk space immediately.
SELECT schemaname, relname, n_live_tup, n_dead_tup
FROM pg_stat_all_tables
Node-postgres is a non-blokcing PostgreSQL client for Node.js. Essentially, node-postgres is a collection of Node.js modules for interfacing with a PostgreSQL database.
First, we need to install PostgreSQL. psql is the PostgreSQL interactive terminal. Running psql will connect you to a PostgreSQL host.
-
--h, --host=HOSTNAME -- database server host or socket directory (default "local socket"),
-
--p, --port=PORT - database serverport (default "5432"),
-
--U, --username=USERNAME -- database username (default: "your_username"),
-
--w, --no-password -- never prompt for a password,
-
--W, --password -- force password prompt (default).
To connect to a database, use the following command:
TRUNCATE <table_name>;
Commands in psql start with a backslash. We can ensure what database, user, and port we've connected to by using:
psql postgres
The following are the most common commands:
-
\q -- exite psql connection,
-
\c -- connect to a new database,
-
\dt -- list all tables,
-
\du -- list all roles,
-
\list -- list databases.
Create a file called queries.js and set up the configuration of your PostgreSQL connection:

In a production environment, you would want to put your configuration details in a separate file with restrictive permissions that is not accessible from version control.
Within queries.js, we need to write functions that interact with the db:

If we wanted to add variables to the query:

We now need to export our CRUD functions:

We then require these functions in our Express API:




