Express Part 2

Express.js - Part 2

Deployment, Forms, & Databases

CSCI 4513

Week 14, Lecture 25

Today's Learning Objectives

Norse Mythology Connection

The Dwarven Forges of Svartalfheim

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 Forge, Vault, and Delivery

// 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
});

Reflection Questions

Part 1: Deployment

Making Your Apps Public

What Are Hosting Providers?

GitHub Pages is great for static sites, but we need more power for Node.js apps!

Static vs Dynamic Sites

πŸ“„ Static Sites

  • Pre-written HTML pages
  • Everyone sees same content
  • Only need: HTML, CSS, JS
  • Examples: GitHub Pages, Netlify

⚑ Dynamic Sites

  • Content changes per user
  • Personalized experiences
  • Need: Server + Database
  • Examples: Twitter/X, Facebook

What is a PaaS?

Landlord metaphor: They handle utilities, maintenance, security. You focus on living in the space!

How PaaS Services Work

Instances

Instance: A single instance of your application running

  • 1 instance = 1 "computer" running your app
  • Like running on localhost, but online
  • Multiple instances = handle more traffic
  • 1 instance is plenty for learning projects!

Many PaaS providers give you your first instance free!

Databases on PaaS

⚠️ You can keep server and database on one PaaS or split them!

Domain Names

Default: Random domain from provider

Example: afternoon-falls-4209.herokuapp.com

Custom domains: (optional)

  • Purchase from registrar (Porkbun, NameSilo)
  • Point to your PaaS project
  • Not needed for portfolio projects!

Find domains with Domainr

Recommended PaaS Providers

Why multiple options?

Heroku discontinued their free tier in 2022 😒

All have limited free tiers, so we recommend:

  • Free: Use combination of providers
  • Paid: Pick one and master it ($5-21/month)

Railway.app

βœ… Can deploy both servers and databases

βœ… Links to GitHub repo

βœ… Pay for what you use ($5/month β‰ˆ 4 apps)

Free Plan:

  • $5 one-time grant (never sleep!)
  • After 30 days or $5 used β†’ limited trial

Render

βœ… Can deploy both servers and databases

βœ… Deploy via "Blueprints" + GitHub

βœ… Free 750 hours/month

⚠️ Databases cost $7 each (lowest)

Free Plan:

  • 750 hours = enough for a few apps
  • Apps sleep after 15 minutes inactivity

Koyeb & Database-Only Options

Koyeb

  • Git push to deploy
  • 1 free web service
  • 1 free Postgres DB (50 hrs)
  • No credit card required

Database Only

Neon:

  • 10 projects
  • 24/7 main database

Aiven:

  • 5 GiB storage
  • 24/7 all services

Node Version Compatibility

// 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!

Debugging Deployment Issues

Don't panic! Most errors are well-documented. Google the error message!

Build Logs

When: Error during deployment

Where: Stream of output after deployment

What to do:

  1. Find the error in the logs
  2. Read the stack trace
  3. Google the error message
  4. Check deployment guide again

Application Logs

When: App deployed but showing errors (500 page)

What: Real-time output of your running app

How to use:

  1. Open application logs
  2. Refresh your app in browser
  3. Watch logs for errors in real-time
  4. Debug based on error messages

Keep Secrets Safe!

⚠️ NEVER commit credentials to git!

  • Use environment variables
  • Add .env to .gitignore
  • Use your PaaS provider's environment variable settings

Part 2: Forms and Data Handling

Collecting and Validating User Input

HTML Forms Refresher

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

POST vs GET for Forms

POST (most common)

  • Creating/updating data
  • Sensitive information
  • Data in request body
  • Not in server logs

GET

  • Search forms
  • Non-modifying operations
  • Data in query string
  • Bookmarkable URLs

Post/Redirect/Get Pattern

// 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!

Validation vs Sanitization

Validation

Checks if input meets criteria

  • Required fields
  • Correct format
  • Length limits
  • Type checking

Sanitization

Cleans malicious data

  • Remove dangerous chars
  • Encode HTML entities
  • Trim whitespace
  • Normalize data

Installing express-validator

// Install
npm install express-validator

// Import
const { body, validationResult, matchedData } = require("express-validator");

The body() Function

// 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");

XSS Attack Example

<!-- ❌ 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: &lt;script&gt;alert(&quot;Hacked!&quot;);&lt;/script&gt;
</div>

When to Escape

Why escape at output, not input?

  • "Dangerous" depends on context
  • What's dangerous for HTML β‰  dangerous for SQL
  • Only dangerous at point of use

EJS: Use <%= %> for escaped output

Use <%- %> only for trusted HTML!

Validation Results

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");
};

Complete Validation Example

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
});

Displaying Errors in View

<!-- 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>

Part 3: PostgreSQL

Data Persistence with Databases

Introduction to PostgreSQL

Watch this short introduction by Fireship

Why Databases?

Where does The Odin Project store usernames? Project submissions? This lesson content? Databases!

PostgreSQL Shell (psql)

Setting Up a Database

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

Identity Columns

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

Populating the Database

-- Insert data
INSERT INTO usernames (username)
VALUES ('Mao'), ('nevz'), ('Lofty');

-- Verify
SELECT * FROM usernames;

--  id | username
-- ----+----------
--   1 | Mao
--   2 | nevz
--   3 | Lofty

Installing node-postgres

npm install pg

Connecting to PostgreSQL

// 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"
});

Client vs Pool

Client

  • Individual connection
  • Manually manage
  • Open, query, close
  • Good for one-off queries

Pool βœ…

  • Pool of connections
  • Automatically managed
  • Reuses connections
  • Perfect for web servers!

Querying with pg

// 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
};

SQL Injection Prevention

// ❌ 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!

Using Queries in Controllers

// 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 };

Populate DB via Script

#! /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();

Running the Population Script

// 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!

Local vs Production Databases

Local DB

  • Development
  • Faster interactions
  • Easy modifications
  • No internet needed
  • Testing features

Production DB

  • Public apps
  • Global access
  • Scalability
  • Robust security
  • External server

Environment Variables

// 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

# .gitignore
node_modules/
.env

# NEVER commit .env to git!
# It contains sensitive credentials

Populating Production DB

// 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>

Project: Inventory Application

Build an inventory management app!

  • Choose what kind of business (groceries, toys, music, etc.)
  • Categories and items
  • Full CRUD for both
  • PostgreSQL database
  • Form validation

Project Requirements

Database Design Examples

Game Management:

  • Tables: games, genres, developers
  • Relationships: game has multiple genres/developers

Pokemon Management:

  • Tables: pokemon, trainers, types
  • Relationships: pokemon has type, trainer has multiple pokemon

Plan your tables, fields, and relationships BEFORE coding!

Delete Functionality

What happens when deleting a category with items?

  • CASCADE: Delete all items in category
  • SET NULL: Remove category from items
  • RESTRICT: Prevent deletion if items exist

Choose based on your app's requirements!

Extra Credit

Review: Deployment

Review: Forms and Validation

Review: PostgreSQL

Complete CRUD Example

// 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);

Security Best Practices

Today's Takeaways

You Can Now:

Next Steps

πŸ“… Next Class: Authentication

Topics Coming:

  • User authentication
  • Password hashing with bcrypt
  • Sessions and cookies
  • Passport.js

πŸ“ Homework: Inventory Application

  • Design database schema
  • Build all CRUD operations
  • Add form validation
  • Deploy with database
  • Extra credit: Add admin password protection