Skip to content

Beginner · Building the backend · topic 10 of 34 · 30 min

The SQL behind each endpoint

Explanation

Every endpoint in the contract runs one or two SQL queries. Listing, reading, searching, creating, changing and deleting notes each have a matching query, and every one of them checks user_id so people only touch their own notes. Values from the user always go in through placeholders, never pasted into the SQL text.

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.

EndpointSQLOwnership check
GET /api/notesSELECT ... WHERE user_id = ? ORDER BY pinned DESC, updated_at DESCuser_id = ?
GET /api/notes?q=milkSELECT ... WHERE user_id = ? AND (title LIKE ? OR body LIKE ?)user_id = ?
POST /api/notesINSERT 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/:idSELECT ... WHERE id = ? AND user_id = ?id = ? AND user_id = ?
PATCH /api/notes/:idUPDATE notes SET title = ?, updated_at = ? WHERE id = ? AND user_id = ?id = ? AND user_id = ?
DELETE /api/notes/:idDELETE 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.

Think of it like this: Think of a bank deposit slip. The bank prints the form; you only write in the boxes for name and amount. Even if you write "and also give me 1 lakh" in the amount box, the cashier reads it as a (strange) amount, not as an instruction. Placeholders are the boxes on the form. Joining strings is like handing the cashier a blank paper and letting the customer write the whole instruction.
javascript
// 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 + '%'],
);
Runs on the server with Node.js. SQL injection means a user sneaks their own SQL into your query.

Listing, reading and searching

sql
-- 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;
pinned DESC puts 1 (pinned) before 0. The LIKE value is sent as %milk%; % means "any characters".

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

sql
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 = ?;
UPDATE and DELETE always carry the owner check in WHERE. The driver tells you how many rows changed: 0 means "not found".

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.

Tip: SQLite's LIKE ignores upper and lower case for English letters, but PostgreSQL's LIKE does not. In PostgreSQL use ILIKE for a case-insensitive search.
Careful: Never build SQL by joining strings with anything that came from a request: search text, ids, sort order. One forgotten spot is enough for SQL injection. Use placeholders everywhere, even for numbers.

Watch the lesson

The SQL behind each endpoint · 1/6

CRUD INSERT / SELECT / UPDATE / DELETE

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)

Task 1 · Code · SQL

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.

Task 2 · Code · SQL

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.

Task 3 · Code · SQL

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

Question 1

1.Which WHERE clause should the query for GET /api/notes/:id use?

Question 2

2.Why use placeholders (? or $1) instead of joining strings?

Question 3

3.What is wrong with: WHERE user_id = 1 AND title LIKE ? OR body LIKE ?

Question 4

4.With 10 notes per page, what gives page 3?

Question 5

5.An UPDATE ... WHERE id = ? AND user_id = ? changes 0 rows. What should the API answer?

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