Beginner · Building the backend · topic 9 of 34 · 30 min
Creating the database: schema, migrations and seed data
Explanation
From design to a real database
The schema is the full list of tables, columns and rules (NOT NULL, UNIQUE, foreign keys) in a database. In module 2 you wrote it down. To make it real, you run the CREATE TABLE statements against a database.
You could type them once into a database tool and forget them. That breaks quickly: your teammate's laptop, the test server and the production server would each need the same typing, and nobody would know which changes each one already has.
-- migrations/001_create_users_notes.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT
);
CREATE TABLE notes (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
body TEXT,
pinned INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT
);What is a migration?
A migration is a small file that changes the schema by one step: create a table, add a column, add an index. Files are numbered (001, 002, 003...) or start with a date, so everyone runs them in the same order. They live in the git repository next to the code.
- A migration runner (a small program) reads the migrations folder.
- It looks at a tracking table in the database, often called schema_migrations, which lists the files already applied.
- It runs only the missing files, oldest first, usually each inside a transaction (all of it happens, or none of it).
- After each file succeeds, it writes the file name into the tracking table, so it never runs twice.
Up and down
The "up" part of a migration makes the change. Some tools also let you write a "down" part that undoes it, useful on your laptop when you want to try again. On a live server, teams usually fix a mistake with a new "up" migration instead.
-- migrations/002_add_archived.sql
-- up
ALTER TABLE notes ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
-- down (undo)
ALTER TABLE notes DROP COLUMN archived;ALTER TABLE changes an existing table. When you add a NOT NULL column to a table that already has rows, give it a DEFAULT, or the database will refuse because the old rows would have no value.
| Tool | How migrations are written | Good to know |
|---|---|---|
| Plain SQL files + a small script | You write each .sql file by hand | Simple and clear; you write the runner yourself (you will, in the task below) |
| Prisma Migrate | You edit schema.prisma; Prisma writes the SQL file | prisma migrate dev on your laptop, prisma migrate deploy on the server |
| Drizzle Kit | You edit TypeScript table definitions; drizzle-kit generate writes SQL | drizzle-kit migrate applies the files |
| Knex | JavaScript files with up and down functions | knex migrate:latest and knex migrate:rollback |
Seed data
Seed data is a set of sample rows (a few users and notes) loaded into a development or test database so the app has something to show. Keep seeds in a separate file or script, not inside migrations, because migrations also run on production and production must not get fake users.
-- seeds/dev.sql (development only)
INSERT INTO users (id, name, email, password_hash, created_at)
VALUES (1, 'Asha', 'asha@example.com', 'hash-a', '2026-01-05 10:00');
INSERT INTO notes (user_id, title, body, pinned, created_at, updated_at)
VALUES (1, 'Physics formulas', 'Ohm law: V = IR', 1, '2026-02-01 08:00', '2026-02-01 08:00');Watch the lesson
Creating the database: schema, migrations and seed data · 1/7
In module 2 we designed the tables on paper. Now we create them for real, in a way the whole team can repeat.
Prefer a video by a teacher? Find free videos on “database migrations explained for beginners sql” on YouTube ↗
In real life: A college results site adds a new column
A college results site stores students and marks. One year the university adds grace marks. The developer does not open the live database and type commands. She writes migration 014_add_grace_marks.sql (ALTER TABLE marks ADD COLUMN grace INTEGER NOT NULL DEFAULT 0), runs all migrations on her laptop, then on the test server, and the deploy script runs the same file on production. Every copy of the database now has the column, and the tracking table shows exactly when it arrived.
Practice (0/3 done)
Only the users table exists (Asha is user 1). 1) Create the notes table with the columns id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), title TEXT NOT NULL, body TEXT, pinned INTEGER DEFAULT 0, created_at TEXT, updated_at TEXT. 2) Insert note 1 for Asha: title "Exam dates", body "Maths on 12 March", both timestamps "2026-03-01 10:00", and do NOT give a pinned value, so the default is used. 3) Insert note 2 for Asha: title "Fees due", body "Pay by 15 March", pinned 1, same timestamps. 4) Finally SELECT id, title, pinned FROM notes, sorted by id.
The Notes database already has data. Write migration 002 and use it. 1) Add a column archived to notes: INTEGER NOT NULL with DEFAULT 0. 2) Archive Asha's "Shopping list" note (id 2) by setting archived to 1. 3) Finally SELECT id, title, archived for all of Asha's notes (user_id = 1), sorted by id.
Write the heart of a migration runner. pendingMigrations(all, applied) gets all = the file names in the migrations folder (in any order) and applied = the names already in the schema_migrations table. Return a NEW array of the files not applied yet, sorted by name (so 001 runs before 002). Do not change the input arrays.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.