CSCI 4513
Week 14, Lecture 25
Deep in Svartalfheim, master dwarven smiths craft legendary treasures, validate their quality, store them in secure vaults, and share them with the Nine Realmsβjust as we deploy applications, validate forms, store data in databases, and make our creations public.
The Tale:
In the dark realm of Svartalfheim dwell the dwarves, the greatest craftsmen in all of creation. Brothers Brokk and Sindri forge in their workshop, creating treasures of immense powerβMjΓΆlnir (Thor's hammer), Gungnir (Odin's spear), and Draupnir (Odin's self-replicating ring). But their work doesn't end at creation.
Each item must pass rigorous quality checks (validation). MjΓΆlnir must be perfectly balanced, Gungnir must never miss its mark, Draupnir must replicate without flaw. Once validated, these treasures are stored in secure vaults (databases) with detailed records of their properties and lineage. Finally, they must be delivered to the surface realms (deployment), where gods and heroes can use them. The dwarves protect their vaults fiercely, ensuring only authorized beings can access or modify their treasuresβmuch like we secure our databases and validate user input.
// The Dwarven Forge - creating items
app.post('/items', validateItem, async (req, res) => {
const { name, power, owner } = matchedData(req);
// Store in the vault (database)
await db.insertItem({ name, power, owner });
// Deliver to the realm (deployment)
res.redirect('/items');
});
// Validation - quality checks
const validateItem = [
body('name').trim()
.notEmpty().withMessage('Item must have a name')
.isLength({ max: 100 }),
body('power').isInt({ min: 1, max: 100 })
.withMessage('Power must be between 1 and 100'),
];
// The Vault - PostgreSQL database
const pool = new Pool({
host: 'localhost',
database: 'dwarven_treasury',
user: 'brokk',
password: process.env.DB_PASSWORD
});
GitHub Pages is great for static sites, but we need more power for Node.js apps!
Landlord metaphor: They handle utilities, maintenance, security. You focus on living in the space!
Instance: A single instance of your application running
Many PaaS providers give you your first instance free!
β οΈ You can keep server and database on one PaaS or split them!
Default: Random domain from provider
Example: afternoon-falls-4209.herokuapp.com
Custom domains: (optional)
Find domains with Domainr
Why multiple options?
Heroku discontinued their free tier in 2022 π’
All have limited free tiers, so we recommend:
β Can deploy both servers and databases
β Links to GitHub repo
β Pay for what you use ($5/month β 4 apps)
Free Plan:
β Can deploy both servers and databases
β Deploy via "Blueprints" + GitHub
β Free 750 hours/month
β οΈ Databases cost $7 each (lowest)
Free Plan:
Neon:
Aiven:
// package.json - specify Node version
{
"name": "my-app",
"version": "1.0.0",
"engines": {
"node": ">=18.0.0"
}
}
Different providers support different Node versions. Check their docs!
Don't panic! Most errors are well-documented. Google the error message!
When: Error during deployment
Where: Stream of output after deployment
What to do:
When: App deployed but showing errors (500 page)
What: Real-time output of your running app
How to use:
β οΈ NEVER commit credentials to git!
<form action="/create" method="POST">
<label for="fullName">Full Name:</label>
<input type="text" name="fullName" id="fullName"
placeholder="John Doe" required>
<button type="submit">Submit</button>
</form>
action: Where data is sentmethod: HTTP verb (GET or POST)name: Key in req.body// User submits form (POST)
app.post('/create', (req, res) => {
// Process data
db.createUser(req.body);
// Redirect to prevent duplicate submissions
res.redirect('/');
});
// User sees result (GET)
app.get('/', (req, res) => {
res.render('index');
});
This prevents duplicate POST requests when refreshing!
Checks if input meets criteria
Cleans malicious data
// Install
npm install express-validator
// Import
const { body, validationResult, matchedData } = require("express-validator");
// Basic validation
body("name")
.trim()
.notEmpty()
.withMessage("Name cannot be empty");
// Optional field with format validation
body("birthdate", "Must be a valid date")
.optional({ values: "falsy" })
.isISO8601(); // YYYY-MM-DD format
// Chaining multiple validations
body("name")
.trim()
.notEmpty().withMessage("Name cannot be empty")
.isAlpha().withMessage("Name must only contain letters")
.isLength({ min: 1, max: 10 })
.withMessage("Name must be 1-10 characters");
<!-- β DANGEROUS - Unescaped output -->
<div>
About Me: <%- description %>
</div>
<!-- User inputs: <script>alert("Hacked!");</script> -->
<!-- Renders as: -->
<div>
About Me: <script>alert("Hacked!");</script>
</div>
<!-- β
SAFE - Escaped output -->
<div>
About Me: <%= description %>
</div>
<!-- Renders as: -->
<div>
About Me: <script>alert("Hacked!");</script>
</div>
Why escape at output, not input?
EJS: Use <%= %> for escaped output
Use <%- %> only for trusted HTML!
const controller = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).render("index", {
errors: errors.array(),
});
}
// Validation passed - use matchedData for safety
const { firstName, lastName } = matchedData(req);
// Do stuff if successful
res.redirect("/success");
};
const alphaErr = "must only contain letters.";
const lengthErr = "must be between 1 and 10 characters.";
const validateUser = [
body("firstName").trim()
.isAlpha().withMessage(`First name ${alphaErr}`)
.isLength({ min: 1, max: 10 }).withMessage(`First name ${lengthErr}`),
body("lastName").trim()
.isAlpha().withMessage(`Last name ${alphaErr}`)
.isLength({ min: 1, max: 10 }).withMessage(`Last name ${lengthErr}`),
];
// Use in route
app.post("/create", validateUser, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).render("createUser", {
errors: errors.array()
});
}
// Process valid data
});
<!-- views/partials/errors.ejs -->
<% if (locals.errors) {%>
<ul class="errors">
<% errors.forEach(function(error) { %>
<li><%= error.msg %></li>
<% }); %>
</ul>
<% } %>
<!-- Include in your form view -->
<%- include("partials/errors.ejs") %>
<form method="POST" action="/create">
<!-- form fields -->
</form>
Watch this short introduction by Fireship
Where does The Odin Project store usernames? Project submissions? This lesson content? Databases!
-- Enter PostgreSQL shell
$ psql
-- Create a new database
CREATE DATABASE top_users;
-- List databases
\l
-- Connect to database
\c top_users
-- Create a table
CREATE TABLE usernames (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
username VARCHAR(255)
);
-- List tables
\d
-- GENERATED ALWAYS AS IDENTITY
-- Automatically generates values for id column
-- Starts at 1, increments by 1
CREATE TABLE users (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
username VARCHAR(255)
);
-- PostgreSQL creates a sequence: users_id_seq
-- Tracks next value to use
-- Insert data
INSERT INTO usernames (username)
VALUES ('Mao'), ('nevz'), ('Lofty');
-- Verify
SELECT * FROM usernames;
-- id | username
-- ----+----------
-- 1 | Mao
-- 2 | nevz
-- 3 | Lofty
npm install pg
// db/pool.js
const { Pool } = require("pg");
// Method 1: Connection object
module.exports = new Pool({
host: "localhost",
user: "<role_name>",
database: "top_users",
password: "<role_password>",
port: 5432 // Default PostgreSQL port
});
// Method 2: Connection URI
module.exports = new Pool({
connectionString: "postgresql://<user>:<password>@localhost:5432/top_users"
});
// db/queries.js
const pool = require("./pool");
async function getAllUsernames() {
const { rows } = await pool.query("SELECT * FROM usernames");
return rows;
}
async function insertUsername(username) {
await pool.query("INSERT INTO usernames (username) VALUES ($1)", [username]);
}
module.exports = {
getAllUsernames,
insertUsername
};
// β DANGEROUS - SQL Injection vulnerability
await pool.query(
"INSERT INTO usernames (username) VALUES ('" + username + "')"
);
// User inputs: sike'); DROP TABLE usernames; --
// Executes: INSERT INTO usernames (username) VALUES ('sike');
// DROP TABLE usernames; --')
// β
SAFE - Parameterized query
await pool.query(
"INSERT INTO usernames (username) VALUES ($1)",
[username]
);
// pg handles escaping and prevents injection!
// controllers/usersController.js
const db = require("../db/queries");
async function getUsernames(req, res) {
const usernames = await db.getAllUsernames();
console.log("Usernames:", usernames);
res.send("Usernames: " +
usernames.map(user => user.username).join(", "));
}
async function createUsernamePost(req, res) {
const { username } = req.body;
await db.insertUsername(username);
res.redirect("/");
}
module.exports = { getUsernames, createUsernamePost };
#! /usr/bin/env node
const { Client } = require("pg");
const SQL = `
CREATE TABLE IF NOT EXISTS usernames (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
username VARCHAR(255)
);
INSERT INTO usernames (username)
VALUES ('Bryan'), ('Odin'), ('Damon');
`;
async function main() {
console.log("seeding...");
const client = new Client({
connectionString: "postgresql://user:pass@localhost:5432/top_users"
});
await client.connect();
await client.query(SQL);
await client.end();
console.log("done");
}
main();
// Drop existing table
DROP TABLE usernames;
// Run script
node db/populatedb.js
// Or add to package.json scripts:
"scripts": {
"db:populate": "node db/populatedb.js"
}
// Then run:
npm run db:populate
β οΈ This script is designed to run once!
// Install dotenv
npm install dotenv
// .env file
DB_HOST=localhost
DB_USER=myuser
DB_PASSWORD=mypassword
DB_NAME=top_users
DB_PORT=5432
// db/pool.js
require('dotenv').config();
module.exports = new Pool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT
});
# .gitignore
node_modules/
.env
# NEVER commit .env to git!
# It contains sensitive credentials
// Use process.argv for flexibility
const connectionString = process.argv[2];
async function main() {
const client = new Client({ connectionString });
await client.connect();
await client.query(SQL);
await client.end();
}
main();
// Local DB:
node db/populatedb.js postgresql://user:pass@localhost:5432/top_users
// Production DB (run once after deployment):
node db/populatedb.js <production-db-url>
Build an inventory management app!
Game Management:
Pokemon Management:
Plan your tables, fields, and relationships BEFORE coding!
What happens when deleting a category with items?
Choose based on your app's requirements!
// CREATE
router.get("/create", renderCreateForm);
router.post("/create", validateItem, createItem);
// READ
router.get("/", listAll);
router.get("/:id", getOne);
// UPDATE
router.get("/:id/update", renderUpdateForm);
router.post("/:id/update", validateItem, updateItem);
// DELETE
router.post("/:id/delete", deleteItem);
π Next Class: Authentication
Topics Coming:
π Homework: Inventory Application