Beginner · Building the backend · topic 10 of 34 · 30 min
The SQL behind each endpoint
Explanation
One query per job
CRUD stands for Create, Read, Update, Delete: the four basic things an app does with data. In SQL they are INSERT, SELECT, UPDATE and DELETE. Each endpoint from the module 2 contract maps to one of them.
| Endpoint | SQL | Ownership check |
|---|---|---|
| GET /api/notes | SELECT ... WHERE user_id = ? ORDER BY pinned DESC, updated_at DESC | user_id = ? |
| GET /api/notes?q=milk | SELECT ... WHERE user_id = ? AND (title LIKE ? OR body LIKE ?) | user_id = ? |
| POST /api/notes | INSERT INTO notes (user_id, title, body, pinned, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) | user_id comes from the session, never from the body |
| GET /api/notes/:id | SELECT ... WHERE id = ? AND user_id = ? | id = ? AND user_id = ? |
| PATCH /api/notes/:id | UPDATE notes SET title = ?, updated_at = ? WHERE id = ? AND user_id = ? | id = ? AND user_id = ? |
| DELETE /api/notes/:id | DELETE FROM notes WHERE id = ? AND user_id = ? | id = ? AND user_id = ? |
Placeholders: the most important habit
A placeholder is a marker in the SQL text, ? in SQLite and MySQL or $1, $2 in PostgreSQL, where a value will go. You send the SQL and the values separately. The database treats the values only as data, never as SQL commands.
// DANGEROUS: the search text becomes part of the SQL
const sql = "SELECT * FROM notes WHERE user_id = 1 AND title LIKE '%" + q + "%'";
// If q is %' OR 1=1 -- the query returns EVERY user's notes.
// SAFE: placeholders (better-sqlite3 for SQLite)
const rows = db
.prepare('SELECT * FROM notes WHERE user_id = ? AND title LIKE ?')
.all(userId, '%' + q + '%');
// SAFE: placeholders (node-postgres for PostgreSQL)
const result = await pool.query(
'SELECT * FROM notes WHERE user_id = $1 AND title LIKE $2',
[userId, '%' + q + '%'],
);Listing, reading and searching
-- List: my notes, pinned first, then the most recently changed
SELECT id, title, body, pinned, created_at, updated_at
FROM notes
WHERE user_id = ?
ORDER BY pinned DESC, updated_at DESC;
-- Read one: only if it is mine
SELECT id, title, body, pinned, created_at, updated_at
FROM notes
WHERE id = ? AND user_id = ?;
-- Search: note the brackets around the OR
SELECT id, title FROM notes
WHERE user_id = ? AND (title LIKE ? OR body LIKE ?)
ORDER BY pinned DESC, updated_at DESC;AND is stronger than OR in SQL, just as multiply is stronger than add in maths. Without brackets, "user_id = ? AND title LIKE ? OR body LIKE ?" means "(my notes with the word in the title) OR (anyone's notes with the word in the body)". That leaks other users' notes. Always put brackets around the OR part.
If the read-one query returns no row, the note either does not exist or belongs to someone else. The server answers 404 in both cases, so a stranger cannot even learn that note 42 exists.
Creating, changing and deleting
INSERT INTO notes (user_id, title, body, pinned, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?);
UPDATE notes
SET title = ?, body = ?, pinned = ?, updated_at = ?
WHERE id = ? AND user_id = ?;
DELETE FROM notes
WHERE id = ? AND user_id = ?;Every UPDATE also sets updated_at to the current time, so the "newest first" order stays correct. After an INSERT, the driver gives you the new id (lastInsertRowid in better-sqlite3, or add RETURNING id in PostgreSQL and SQLite).
Paging with LIMIT and OFFSET
A user with 2,000 notes should not get all of them in one response. LIMIT says how many rows to return; OFFSET says how many to skip first. For page p (starting at 1) with size n: LIMIT n OFFSET (p - 1) * n. Paging needs a fixed ORDER BY, or rows can jump between pages.
Watch the lesson
The SQL behind each endpoint · 1/6
Each endpoint in our contract runs a SQL query. Create, read, update and delete become INSERT, SELECT, UPDATE and DELETE.
Prefer a video by a teacher? Find free videos on “sql crud queries insert update delete where limit offset tutorial” on YouTube ↗
In real life: Order history in a food delivery app
In a food delivery app, the "Your orders" screen runs one query: orders WHERE customer_id = the logged-in customer, newest first, 10 at a time with LIMIT and OFFSET as you scroll. The "Order details" screen runs WHERE id = ? AND customer_id = ?. If a curious customer changes the order number in the address bar, the second check returns no row and the app shows "Order not found" instead of a stranger's address and phone number.
Practice (0/3 done)
Write the query behind GET /api/notes for Asha (user_id 1). Return id, title and pinned of her notes only, pinned notes first, then the most recently updated first (updated_at newest first). In the real server the 1 would be a placeholder; here write the number.
Asha (user_id 1) searches for "formula". Return id and title of HER notes where the title OR the body contains "formula", sorted by id. Careful: another user also has a note with "formula" in it, and it must not appear.
Run these statements in order, each with an ownership check in WHERE. 1) Asha (user 1) renames her note 2 to "Weekly shopping" and sets updated_at to "2026-03-01 10:00". 2) Ravi (user 2) tries to delete note 1, which is Asha's: write the DELETE as Ravi would send it (id = 1 AND user_id = 2), so it deletes nothing. 3) Asha deletes her note 8. 4) Finally SELECT id, title, updated_at of Asha's notes, sorted by id.
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.
← Previous: Creating the database: schema, migrations and seed data