Skip to main content
Dublin Library

The Publishing Project

Building a Recipe Database

 

In a previous post we looked at how to create a database in a Homebrew version of Postgresql.

This post will look at the database structure of a hypothetical license database web application as an example of building the schema and the database in Postgres 17 along with some necessary concepts when building the schema and table.

Database Overview #

  • recipe
    • id unique primary key
    • name name of the recipe for example carrot cake
    • description short descriptive text about the recipe
    • instructions step by step on how to make it
    • created_at date when the recipe was inserted into the database
  • ingredient
    • id unique primary key
    • name name of the ingredient for example butter
  • measure
    • id unique primary key
    • name name of the measure for example cup

We want each recipe to have multiple ingredients in different measurements (like 2 coups of flour). To achieve this we need a table that reference items on the other tables.

  • recipe_ingredient
    • recipe_id reference to a recipe
    • ingredient_id reference to an ingredient
    • measure_id reference to a measure
    • amount how much of the ingredient should be added

SQL Data Types #

These are the types of data values that we'll use with the database.

When creating databases there are times when specific types will perform best and others when saving storage space is essential. It's a trade off that you have to evaluate every time you build a database.

The text types we use are integer, an integer numeric value and serial, an auto increasing decimal value, used mostly for IDs.

Type Description Storage Size Range Example
integer A 4-byte integer type (default integer type). 4 bytes -2,147,483,648
to
2,147,483,647 -5000,
0, 150000
serial An auto-incrementing 4-byte integer type, often used for primary keys. 4 bytes Same as integer. Automatically increments by 1

For text fields, we usevarchar() for text fields of text of nore more than the specified length (varchar(255) will be no longer than 255 characters), and text, an unlimited text field

Type Description Storage Size Length Example
varchar(n) A variable-length character type. It can store strings with a maximum length of n. 1 byte + actual length 1 to 8,000 'Alice', 'Bob'
text A variable-length character type with no specific length limit. 1 + actual length (1 byte for length) No limit 'This is a long text string.'

We use timestamp to capture date and time values. In this database we use it to capture the date the recipe was created.

Type Description Storage Size Range Example
timestamp Stores both date and time (without time zone). 8 bytes 4713 BC
to
5874897 AD
2024-10-08 14:30:00

SQL code #

This is the SQL code that describes the database we want to create.

The first table is the main recipe table with the following elements:

Name Type Notes
id serial primary key
name varchar(250)
description varchar(250)
instruction text The full text of the recipe
created_at timestamp Not null

defaults to current time
create table recipe (
  id SERIAL PRIMARY KEY,
  name VARCHAR(250),
  description VARCHAR(250),
  instructions text,
  created_at timestamp NOT NULL DEFAULT NOW());

The ingredient table has two elements. Each ingredient is unique, there can't be duplicate ingredients.

Name Type Notes
id serial primary key
name varchar(100) unique
create table ingredient (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) UNIQUE);

The measure table has two elements. Each ingredient is unique, there can't be duplicate neasurements.

Name Type Notes
id serial primary key
name varchar(100) unique
create table measure (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) UNIQUE);

The recipe_ingredient table uses A foreign key constraint to specify that the values in a column (or a group of columns) must match the values appearing in some row of another table.

We say this maintains the referential integrity between two related tables.

Name Type Notes
recipe_id int not null
ingredient_id int not null
measure_id int not null
amount int

The table also has a set of foreign key constraints

ON DELETE CASCADE is used to specify that when a row is deleted from the parent table, all rows in the child table that reference the deleted row should also be deleted. This is useful for maintaining the integrity of the database.

Name Type Notes
fk_recipe foreign key (recipe_id) References Recipe(id) ON DELETE CASCADE
fk_ingredient FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id)
fk_measure foreign key (measure_id) references Measure(id)
create table recipe_ingredient (
  recipe_id INT NOT NULL,
  ingredient_id INT NOT NULL,
  measure_id INT,
  amount INT,

  CONSTRAINT fk_recipe FOREIGN KEY(recipe_id) REFERENCES Recipe(id) ON DELETE CASCADE,
  CONSTRAINT fk_ingredient FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id),
  CONSTRAINT fk_measure FOREIGN KEY(measure_id) REFERENCES Measure(id));

This will create the necessary database. We can now play with front ends in different languages.

To run a basic test using Node.js we first create a database driver file:

const { Pool } = require('pg');

const pool = new Pool({
  user: 'me',
  host: 'localhost',
  database: 'recipes',
  password: 'password',
  port: 5432,
});

module.exports = {
  query: (text, params, callback) => pool.query(text, params, callback),
};

We then create a basic Express application to validate that the database works as intended.

const express = require('express');
const db = require('./db');

const app = express();
const port = 3000;

app.get('/api', async (req, res) => {
  const queryResult = await db.query('select * from recipe');
  res.send(queryResult.rows);
});

app.listen(port || 2509, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

Edit on Github