Computing FoundationsAPIs and the Web › Day 23

Hands-on lab — Day 23: REST Fundamentals: Resources and Verbs

Commands

Setup

cd labs/sections/computing-foundations/day-023-rest-fundamentals-resources-and-verbs

Run

bash examples/rest_crud.sh
bash starter/rest_crud.sh

Test

bash tests/run_tests.sh

File tree

examples/rest_crud.sh
expected-output/sample-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/rest_crud.sh
starter/rest-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 023 lab — CRUD Against a REST API

Lesson

Purpose

Day 23's lesson built the resource-and-verb model of REST. This lab puts it in your hands: you run a full create-read-update-delete cycle against a real public REST API, using nothing but the HTTP verbs and two resource addresses. You read a representation of an item, list a collection, create a new item and watch the server assign its id, replace an item, and delete one — reading the real status code the server returns for each verb.

Learning objectives

  • Read a resource's JSON representation with GET on an item and a collection.
  • Create a resource by POSTing to the collection and confirm a 201 Created.
  • Replace a resource with PUT and read the 200 OK response.
  • Delete a resource with DELETE and read its status code.
  • Explain why POST targets the collection while GET/PUT/DELETE target an item, and why a practice API's faked writes do not persist.

Prerequisites

  • The Day 23 lesson, Day 18 (HTTP methods), and Day 22 (what an API is).
  • A terminal with curl and network access. python3 is optional (it only pretty-prints the JSON; the lab works without it).

Supported operating systems

  • macOS and Linux — fully supported (curl preinstalled).
  • Windowscurl ships with Windows 10+; or run under WSL.

Hardware requirements

Any computer with an internet connection. The lab only makes ordinary web requests.

Required software

curl only — preinstalled everywhere. python3 is optional. See requirements/README.md.

Free and open-source options

Everything here is free: curl is open source, and the test service (jsonplaceholder.typicode.com) is a free public REST API that needs no account or key.

Installation

None. Change into this directory:

cd labs/sections/computing-foundations/day-023-rest-fundamentals-resources-and-verbs

File structure

day-023-.../
├── README.md
├── metadata.yml
├── starter/
│   ├── rest_crud.sh          ← YOUR working file (4 exercises)
│   └── rest-worksheet.md     ← record your status codes and the new id
├── examples/
│   └── rest_crud.sh          ← completed reference implementation
├── tests/
│   └── run_tests.sh
├── expected-output/
│   └── sample-run.txt        ← real captured run
├── requirements/README.md
├── troubleshooting.md
└── security.md

How to run

## 1. See the finished CRUD cycle first (needs network)
bash examples/rest_crud.sh

## 2. Complete the four exercises in the starter, then run it
bash starter/rest_crud.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/rest_crud.sh — runs the five REST exchanges against jsonplaceholder.typicode.com: GET /posts/1 (read an item), GET /posts (list the collection), POST /posts (create — returns 201 and a new id), PUT /posts/1 (replace — returns 200), and DELETE /posts/1 (remove — returns 200). It pretty-prints JSON with python3 -m json.tool when available and degrades gracefully offline.
  • bash starter/rest_crud.sh — the same skeleton with four numbered exercises; each names the exact curl command to use for GET, POST, PUT, and DELETE.
  • bash tests/run_tests.sh — always verifies structure (files present, both scripts parse, the example exercises each verb, the starter names four exercises), then — when online — checks the real behaviour: GET /posts/1 returns 200 with the expected keys and POST /posts returns 201. Offline, the live checks are skipped (never failed). Exits 0 on success.

Expected output

See expected-output/sample-run.txt for a real captured run. The GET bodies are fixed by the service; the created id is always 101 on JSONPlaceholder.

Validation steps

  1. bash examples/rest_crud.sh prints all five sections without errors.
  2. The POST section shows HTTP 201 and a body containing "id": 101.
  3. The PUT and DELETE sections show HTTP 200.
  4. The tests pass.

Tests

bash tests/run_tests.sh

Expected final line: 15 checks, 0 failure(s), 0 skip(s). online (the three live checks skip, never fail, when offline). Exit 0 on success.

Cleanup

Nothing to clean up — the scripts only make web requests and write no files (and JSONPlaceholder fakes its writes, so nothing persists server-side). Reset your edited starter with git checkout -- starter/rest_crud.sh.

Troubleshooting

See troubleshooting.md — faked writes not persisting, missing python3, status codes stuck to the body, and offline behaviour.

Security notes

See security.md. Only a public test API; never send real secrets to a practice service; keep tokens in headers, never in URLs.

Extension exercises

  1. Send a PATCH /posts/1 with only {"title":"just the title"} and compare the returned representation to what a full PUT returned.
  2. Fetch a nested collection — GET /posts/1/comments and GET /users/1/todos — and note how the URL reads as a path through related resources.
  3. Run the same DELETE /posts/1 twice and confirm the outcome is identical (idempotency), then explain why POST /posts twice would differ.
  • Previous day: Day 22 — What an API Is and Why Everything Has One.
  • Next day: Day 24 — JSON and Data Serialization.

Expected output

sample-run.txt

Day 023 — CRUD Against a REST API
Target: https://jsonplaceholder.typicode.com (a free public REST API that fakes writes)

=== READ one item — GET /posts/1 ===
$ curl -s https://jsonplaceholder.typicode.com/posts/1 | python3 -m json.tool
{
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

=== READ the collection — GET /posts (first entries) ===
$ curl -s https://jsonplaceholder.typicode.com/posts | python3 -m json.tool | head -20
[
    {
        "userId": 1,
        "id": 1,
        "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
        "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
    },
    {
        "userId": 1,
        "id": 2,
        "title": "qui est esse",
        "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
    },
    {
        "userId": 1,
        "id": 3,
        "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
        "body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
    },
    {
... collection contains 100 posts total.

=== CREATE — POST /posts (returns 201 + the created resource) ===
$ curl -s -X POST -H 'Content-Type: application/json' -d '{...}' https://jsonplaceholder.typicode.com/posts
{
    "title": "my new post",
    "body": "written today",
    "userId": 1,
    "id": 101
}
status: HTTP 201  (expect 201 Created — the server assigned the new id)

=== UPDATE — PUT /posts/1 (replace the whole item) ===
$ curl -s -X PUT -H 'Content-Type: application/json' -d '{...}' https://jsonplaceholder.typicode.com/posts/1
{
    "id": 1,
    "title": "edited title",
    "body": "edited body",
    "userId": 1
}
status: HTTP 200  (expect 200 OK — the replaced representation)

=== DELETE — DELETE /posts/1 (remove the item) ===
$ curl -s -X DELETE https://jsonplaceholder.typicode.com/posts/1  -w 'HTTP %{http_code}'
status: HTTP 200  (expect 200 — an empty body; DELETE is idempotent)

Done. Two addresses (/posts and /posts/1) and five verbs drove the whole
lifecycle. Note: JSONPlaceholder fakes writes, so the created id 101 is
not re-fetchable — the status codes are real, the persistence is simulated.

Source files

examples/rest_crud.sh (5633 bytes)
#!/usr/bin/env bash
# Day 023 lab — CRUD Against a REST API (completed reference implementation).
#
# Runs a full create-read-update-delete cycle against JSONPlaceholder
# (https://jsonplaceholder.typicode.com), a free public REST API that accepts
# CRUD requests and responds realistically. It *fakes* writes: a POST returns
# 201 and a new id, but nothing is actually stored — perfect for safe practice.
#
# Each section is one HTTP verb acting on a resource address, and prints the
# status code and the JSON body the server returned.
#
# Run from the lab directory:  bash examples/rest_crud.sh
#
# Requires network access. If the network (or the test server) is unreachable,
# each section degrades gracefully with a clear message and the script still
# exits 0, so it is safe to run offline.
set -u

API="https://jsonplaceholder.typicode.com"

rule() { printf '\n=== %s ===\n' "$1"; }

# Pretty-print JSON on stdin if python3 is available; otherwise pass it through.
pretty() {
  if command -v python3 >/dev/null 2>&1; then
    python3 -m json.tool 2>/dev/null || cat
  else
    cat
  fi
}

# Return 0 if we appear to have network access to the API, else 1.
have_network() {
  curl -s -o /dev/null --max-time 12 "${API}/posts/1" 2>/dev/null
}

# ---------------------------------------------------------------------------
# READ (GET) one item: fetch a representation of post 1 by its address.
# ---------------------------------------------------------------------------
read_item() {
  rule "READ one item — GET /posts/1"
  echo "\$ curl -s ${API}/posts/1 | python3 -m json.tool"
  curl -s --max-time 20 "${API}/posts/1" | pretty \
    || echo "(could not reach ${API})"
}

# ---------------------------------------------------------------------------
# READ (GET) the collection: list posts (show the first few for brevity).
# ---------------------------------------------------------------------------
read_collection() {
  rule "READ the collection — GET /posts (first entries)"
  echo "\$ curl -s ${API}/posts | python3 -m json.tool | head -20"
  curl -s --max-time 20 "${API}/posts" | pretty | head -20 \
    || echo "(could not reach ${API})"
  # Report the total count so the learner sees it is a full collection.
  local count
  count="$(curl -s --max-time 20 "${API}/posts" \
    | grep -o '"id"' | wc -l | tr -d ' ')" || count="?"
  echo "... collection contains ${count} posts total."
}

# ---------------------------------------------------------------------------
# CREATE (POST) to the collection: the server assigns a new id, returns 201.
# ---------------------------------------------------------------------------
create_item() {
  rule "CREATE — POST /posts (returns 201 + the created resource)"
  echo "\$ curl -s -X POST -H 'Content-Type: application/json' -d '{...}' ${API}/posts"
  local body code
  body="$(curl -s --max-time 20 -X POST \
    -H "Content-Type: application/json" \
    -d '{"title":"my new post","body":"written today","userId":1}' \
    "${API}/posts")" || { echo "(could not reach ${API})"; return; }
  code="$(curl -s -o /dev/null --max-time 20 -X POST \
    -H "Content-Type: application/json" \
    -d '{"title":"my new post","body":"written today","userId":1}' \
    -w '%{http_code}' "${API}/posts")" || code="000"
  printf '%s' "${body}" | pretty
  echo "status: HTTP ${code}  (expect 201 Created — the server assigned the new id)"
}

# ---------------------------------------------------------------------------
# UPDATE (PUT) an item: replace post 1 entirely with the body we send.
# ---------------------------------------------------------------------------
update_item() {
  rule "UPDATE — PUT /posts/1 (replace the whole item)"
  echo "\$ curl -s -X PUT -H 'Content-Type: application/json' -d '{...}' ${API}/posts/1"
  local body code
  body="$(curl -s --max-time 20 -X PUT \
    -H "Content-Type: application/json" \
    -d '{"id":1,"title":"edited title","body":"edited body","userId":1}' \
    "${API}/posts/1")" || { echo "(could not reach ${API})"; return; }
  code="$(curl -s -o /dev/null --max-time 20 -X PUT \
    -H "Content-Type: application/json" \
    -d '{"id":1,"title":"edited title","body":"edited body","userId":1}' \
    -w '%{http_code}' "${API}/posts/1")" || code="000"
  printf '%s' "${body}" | pretty
  echo "status: HTTP ${code}  (expect 200 OK — the replaced representation)"
}

# ---------------------------------------------------------------------------
# DELETE an item: remove post 1. Idempotent — repeating it is harmless.
# ---------------------------------------------------------------------------
delete_item() {
  rule "DELETE — DELETE /posts/1 (remove the item)"
  echo "\$ curl -s -X DELETE ${API}/posts/1  -w 'HTTP %{http_code}'"
  local code
  code="$(curl -s -o /dev/null --max-time 20 -X DELETE \
    -w '%{http_code}' "${API}/posts/1")" || code="000"
  echo "status: HTTP ${code}  (expect 200 — an empty body; DELETE is idempotent)"
}

main() {
  echo "Day 023 — CRUD Against a REST API"
  echo "Target: ${API} (a free public REST API that fakes writes)"
  if ! have_network; then
    echo
    echo "OFFLINE: could not reach ${API}. The commands below need network"
    echo "access. Connect to the internet and re-run: bash examples/rest_crud.sh"
    exit 0
  fi
  read_item
  read_collection
  create_item
  update_item
  delete_item
  echo
  echo "Done. Two addresses (/posts and /posts/1) and five verbs drove the whole"
  echo "lifecycle. Note: JSONPlaceholder fakes writes, so the created id 101 is"
  echo "not re-fetchable — the status codes are real, the persistence is simulated."
}

main "$@"
metadata.yml (602 bytes)
lesson_id: D023
day: 23
kind: api-example
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-023-rest-fundamentals-resources-and-verbs
run_commands:
  - bash examples/rest_crud.sh
  - bash starter/rest_crud.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/rest_crud.sh starter/rest-worksheet.md  # optional: reset your work'
requires_network: true
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (curl 8.7.1), online, bash tests/run_tests.sh → 15 checks, 0 failure(s), 0 skip(s)'
requirements/README.md (881 bytes)
# Dependencies — Day 023 lab

**`curl` only** — preinstalled on macOS and mainstream Linux (and available on
Windows 10+ / WSL). `python3` is optional: it only pretty-prints the JSON with
`python3 -m json.tool`, and the scripts fall back to raw output when it is
absent. No package installs, no accounts, no API keys.

The lab reaches one free public REST API — `https://jsonplaceholder.typicode.com`
— so it needs network access to show live output. With no network it degrades
gracefully: the scripts print a clear message and the tests skip (never fail)
the live checks. No sudo required.

Note that JSONPlaceholder **fakes** its writes: a `POST`, `PUT`, or `DELETE`
returns a realistic status code and body, but nothing is stored server-side.
That is intentional and safe for practice — you can run the CRUD cycle as many
times as you like without changing anything.
starter/rest_crud.sh (1957 bytes)
#!/usr/bin/env bash
# Day 023 starter — CRUD Against a REST API.
# Complete the four exercises below. Each names the exact curl command to use.
# Run from the lab directory:  bash starter/rest_crud.sh
# The completed reference is in ../examples/rest_crud.sh — try it yourself first.
set -u

API="https://jsonplaceholder.typicode.com"
rule() { printf '\n=== %s ===\n' "$1"; }

echo "Day 023 starter — CRUD Against a REST API"
echo "Target: ${API}"

# Exercise 1 (READ): fetch one item and pretty-print its JSON representation.
# Replace the echo with:
#   curl -s "$API/posts/1" | python3 -m json.tool
rule "1. READ — GET /posts/1"
echo "EXERCISE — your turn: GET $API/posts/1 and pretty-print the JSON body"

# Exercise 2 (CREATE): POST a new post to the COLLECTION and print the status.
# Replace with:
#   curl -s -w '\nHTTP %{http_code}\n' -X POST \
#     -H "Content-Type: application/json" \
#     -d '{"title":"my post","body":"hello","userId":1}' \
#     "$API/posts"
rule "2. CREATE — POST /posts"
echo "EXERCISE — your turn: POST a JSON body to $API/posts and confirm HTTP 201"

# Exercise 3 (UPDATE): PUT a full replacement to the ITEM and print the status.
# Replace with:
#   curl -s -w '\nHTTP %{http_code}\n' -X PUT \
#     -H "Content-Type: application/json" \
#     -d '{"id":1,"title":"edited","body":"changed","userId":1}' \
#     "$API/posts/1"
rule "3. UPDATE — PUT /posts/1"
echo "EXERCISE — your turn: PUT a full new representation to $API/posts/1 (HTTP 200)"

# Exercise 4 (DELETE): DELETE the ITEM and print the status code.
# Replace with:
#   curl -s -w '\nHTTP %{http_code}\n' -X DELETE "$API/posts/1"
rule "4. DELETE — DELETE /posts/1"
echo "EXERCISE — your turn: DELETE $API/posts/1 and read the status code (HTTP 200)"

echo
echo "When all four are done, compare your output with ../examples/rest_crud.sh"
echo "Remember: POST targets the COLLECTION (/posts); GET, PUT, DELETE target an ITEM (/posts/1)."
starter/rest-worksheet.md (1375 bytes)
# REST worksheet — Day 023

Run the commands (from the examples or your completed starter) against
JSONPlaceholder (`https://jsonplaceholder.typicode.com`) and record what the
server returned. The status codes are real even though the writes are faked.

## 1. Read — status code for a GET

- Command: `curl -s -o /dev/null -w '%{http_code}\n' https://jsonplaceholder.typicode.com/posts/1`
- Status code returned: `____`  (expect 200)

## 2. Create — status code and assigned id for a POST

- Command:
  `curl -s -w '\nHTTP %{http_code}\n' -X POST -H "Content-Type: application/json" -d '{"title":"my post","body":"hello","userId":1}' https://jsonplaceholder.typicode.com/posts`
- Status code returned: `____`  (expect 201)
- The `id` JSONPlaceholder assigned to your new post: `____`

## 3. Delete — status code for a DELETE

- Command: `curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://jsonplaceholder.typicode.com/posts/1`
- Status code returned: `____`  (expect 200)

## 4. Collection vs item

- Which of your four requests targeted the **collection** (`/posts`)? `____`
- Which targeted an **item** (`/posts/1`)? `____`

## 5. One paragraph

Explain, in your own words (4–6 sentences), why you can safely retry a failed
`GET` or `DELETE` but must be careful retrying a failed `POST`. Name the
property that makes the difference.

> _your answer here_
tests/run_tests.sh (4220 bytes)
#!/usr/bin/env bash
# Tests for the Day 023 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Two kinds of checks:
#   * Structure checks always run and must pass (files present, both scripts
#     parse, the example exercises each CRUD verb, the starter names its four
#     exercises).
#   * Network checks run only when the API is reachable. They verify that a
#     GET returns 200 with the expected keys and that a POST returns 201.
#     Offline (or when the public server is transiently unavailable) the live
#     checks are SKIPPED with a message rather than failed — a third-party
#     outage is not the learner's bug.
#
# The script exits 0 when no structure check failed, whether online or offline.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
API="https://jsonplaceholder.typicode.com"
failures=0
checks=0
skips=0

pass() { checks=$((checks + 1)); echo "  ok:   $1"; }
fail() { checks=$((checks + 1)); failures=$((failures + 1)); echo "  FAIL: $1"; }
skip() { skips=$((skips + 1)); echo "  skip: $1"; }

echo "== Structure checks =="
example="${lab_dir}/examples/rest_crud.sh"
starter="${lab_dir}/starter/rest_crud.sh"
worksheet="${lab_dir}/starter/rest-worksheet.md"

[ -s "${example}" ] && pass "example script exists" || fail "example script missing"
[ -s "${starter}" ] && pass "starter script exists" || fail "starter script missing"
[ -s "${worksheet}" ] && pass "worksheet exists" || fail "worksheet missing"

# The example must exercise every CRUD verb and the pretty-printer.
for needle in "GET /posts/1" "POST /posts" "PUT /posts/1" "DELETE /posts/1" "json.tool"; do
  if grep -q -- "${needle}" "${example}"; then pass "example uses '${needle}'"; else fail "example missing '${needle}'"; fi
done

# The starter must name its four numbered exercises.
ex_count="$(grep -cE 'Exercise [1-4]' "${starter}" 2>/dev/null || true)"
if [ "${ex_count:-0}" -ge 4 ]; then pass "starter has 4 numbered exercises"; else fail "starter has 4 numbered exercises (found ${ex_count:-0})"; fi

# Both scripts must be valid bash.
if bash -n "${example}" 2>/dev/null; then pass "example has valid bash syntax"; else fail "example has a syntax error"; fi
if bash -n "${starter}" 2>/dev/null; then pass "starter has valid bash syntax"; else fail "starter has a syntax error"; fi

echo
echo "== Network checks =="
if ! curl -s -o /dev/null --max-time 12 "${API}/posts/1" 2>/dev/null; then
  skip "no network access — skipping all live-request checks (expected offline)"
else
  pass "network reachable (${API} responded)"

  # Check 1: GET /posts/1 returns 200.
  code="$(curl -s -o /dev/null --max-time 20 --retry 3 --retry-delay 1 \
    -w '%{http_code}' "${API}/posts/1" 2>/dev/null)" || code="000"
  case "${code}" in
    200) pass "GET /posts/1 returned HTTP 200" ;;
    5* | 000) skip "GET check — server transiently unavailable (got '${code}'); retry later" ;;
    *) fail "GET /posts/1 should return HTTP 200 (got '${code}')" ;;
  esac

  # Check 2: GET /posts/1 body carries the expected resource keys.
  body="$(curl -s --max-time 20 --retry 3 --retry-delay 1 "${API}/posts/1" 2>/dev/null)" || body=""
  if [ -z "${body}" ]; then
    skip "GET-keys check — server transiently unavailable; retry later"
  elif printf '%s' "${body}" | grep -q '"id"' \
    && printf '%s' "${body}" | grep -q '"title"' \
    && printf '%s' "${body}" | grep -q '"userId"'; then
    pass "GET /posts/1 body has the expected keys (id, title, userId)"
  else
    fail "GET /posts/1 body should contain id, title, and userId"
  fi

  # Check 3: POST /posts returns 201 (create against the collection).
  pcode="$(curl -s -o /dev/null --max-time 20 --retry 3 --retry-delay 1 \
    -X POST -H "Content-Type: application/json" \
    -d '{"title":"t","body":"b","userId":1}' \
    -w '%{http_code}' "${API}/posts" 2>/dev/null)" || pcode="000"
  case "${pcode}" in
    201) pass "POST /posts returned HTTP 201 Created" ;;
    5* | 000) skip "POST check — server transiently unavailable (got '${pcode}'); retry later" ;;
    *) fail "POST /posts should return HTTP 201 (got '${pcode}')" ;;
  esac
fi

echo
echo "${checks} checks, ${failures} failure(s), ${skips} skip(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 023 lab

My POST returned 201 but the new post is not there when I GET it

This is expected. JSONPlaceholder fakes writes. It responds as if it created the resource — a real 201 Created and the id 101 — but nothing is actually stored. If you then GET /posts/101, you get a 404, because the post was never persisted. The status codes and echoed bodies are genuine; only the persistence is simulated. This is exactly what makes the service safe for practice: you can run the full create-update-delete cycle repeatedly without changing any real data.

python3: command not found

The pretty-printer is optional. Use python instead of python3, or drop the pipe entirely — curl -s .../posts/1 still returns valid JSON, just unformatted. The example script already falls back to raw output when python3 is missing.

The status code prints stuck to the JSON body

Keep the newline in the -w format: -w "\nHTTP %{http_code}\n". Without the leading \n, the status code lands on the same line as the last byte of the body.

My POST behaved like a GET (no creation, no 201)

You dropped -X POST or the -d body. The -d flag supplies the request body and, on its own, already switches the method to POST; combine it with a Content-Type: application/json header so the server parses your JSON.

I created to /posts/1 instead of /posts and got a surprise

You create by POSTing to the collection (/posts), not to an item (/posts/1) — the item does not exist yet, and the server assigns the new id. GET, PUT, PATCH, and DELETE target a specific item you already know.

jsonplaceholder.typicode.com is slow or returns a 5xx

It is a shared free service and can occasionally be busy. The example and the tests retry transient errors and, in the tests, skip (never fail) a live check when the server is transiently unavailable. Wait a moment and re-run.

Offline

The scripts and tests detect no network and exit 0 with a clear message. Connect to the internet to see live output.

Security notes

Security notes — Day 023 lab

  • What it does: sends ordinary REST requests (GET, POST, PUT, DELETE) to one public test API (jsonplaceholder.typicode.com) and reads the responses. No data of yours is stored anywhere — the service fakes its writes. No sudo; no files written outside the lab.
  • Never send real secrets to a practice API. The sample bodies here are non-sensitive placeholder values ("title":"my post"). Do not post real API keys, tokens, passwords, or personal data to any public test service.
  • Keep credentials in headers, never in URLs. A real REST API authenticates with a token in the Authorization header. URLs appear in logs, browser history, and proxies, so a secret in a path or query string (/posts?token=...) is a leak. This lab uses no credentials at all.
  • Predictable URLs are enumerable. Because /posts/1, /posts/2, ... are guessable, a real server must authorize every request against the specific resource. When you build your own API, never assume that knowing a URL implies the right to access it.
  • Only call APIs you are allowed to. Public test services like this one invite practice traffic; probing or hammering servers you do not own or have permission to test can violate their terms or the law.