Welcome back, @name! Darwin here with another lesson on data flow.
You've learned data formats (JSON, CSV, XML) and the HTTP protocol (GET, POST, PUT, DELETE). Now it's time for REST API - the standard architecture for building APIs used worldwide!
Safari Analogy: HTTP is the courier system. REST API is the standard procedures - the rules by which you organize research stations, data catalogs, and communication between them. Thanks to these rules, any biologist, regardless of where they come from, knows how to fetch lion data:
GET /api/species/lionREST (REpresentational State Transfer) is an architectural style for APIs - a set of rules and conventions for designing APIs that use HTTP.
API (Application Programming Interface) is a programming interface - the way applications communicate with each other.
REST API = HTTP API designed according to REST principles
Without standards, every API would look different:
/getLion?id=123/fetchLionData/lion-information/get/api/v1/animals/lion/retrieveWith REST everyone knows:
GET /api/species/lion - fetch a lionPOST /api/species - create a speciesDELETE /api/species/lion - delete a lionPredictable, consistent, intuitive!
In REST everything is a resource - an object, entity, or data.
Good - nouns:
/species - species/observations - observations/habitats - habitats/researchers - researchersBad - verbs:
/getSpecies/createObservation/deleteHabitatThe action is determined by the HTTP method, not the URL!
| Method | Action | Example | |--------|--------|---------| | GET | Retrieve resource(s) |
GET /species - list, GET /species/lion - one |
| POST | Create new resource | POST /species - add species |
| PUT | Replace entire resource | PUT /species/lion - replace lion data |
| PATCH | Update part of resource | PATCH /species/lion - change population |
| DELETE | Delete resource | DELETE /species/lion - delete lion |Each request must contain all the information needed to process it. The server does not remember previous requests.
Example:
1# Each request contains the authorization token
2GET /api/species/lion
3Authorization: Bearer abc123
4
5GET /api/observations
6Authorization: Bearer abc123The server doesn't know "who sent this earlier" - each request is self-contained!
Resources can be nested hierarchically:
1/species/lion # Species: lion
2/species/lion/observations # Lion observations
3/species/lion/observations/123 # Specific lion observation #123
4
5/habitats/serengeti # Habitat: Serengeti
6/habitats/serengeti/species # Species in SerengetiRule:
/resource/id/subresource/idAPIs evolve - versioning prevents breaking existing clients:
1/api/v1/species
2/api/v2/speciesChange version when:
1# LIST - list all species
2GET /api/species
3Response: [{"id": "lion", "name": "Lion"}, {"id": "elephant", "name": "Elephant"}]
4
5# GET - one species
6GET /api/species/lion
7Response: {"id": "lion", "scientific_name": "Panthera leo", "population": 120}
8
9# CREATE - create new species
10POST /api/species
11Body: {"scientific_name": "Gorilla gorilla", "population": 230}
12Response: {"id": "gorilla", "scientific_name": "Gorilla gorilla", "population": 230}
13
14# UPDATE (full) - replace species
15PUT /api/species/lion
16Body: {"scientific_name": "Panthera leo", "population": 125, "habitat": "savanna"}
17Response: {"id": "lion", ...}
18
19# UPDATE (partial) - change only population
20PATCH /api/species/lion
21Body: {"population": 125}
22Response: {"id": "lion", "population": 125, ...}
23
24# DELETE - delete species
25DELETE /api/species/lion
26Response: 204 No Content (or 200 with confirmation)1# List observations for a lion
2GET /api/species/lion/observations
3Response: [
4 {"id": "obs1", "date": "2024-01-15", "count": 12},
5 {"id": "obs2", "date": "2024-01-20", "count": 8}
6]
7
8# Specific lion observation
9GET /api/species/lion/observations/obs1
10Response: {"id": "obs1", "species": "lion", "date": "2024-01-15", "count": 12}
11
12# Add observation for a lion
13POST /api/species/lion/observations
14Body: {"date": "2024-01-25", "count": 10, "location": "Serengeti"}
15Response: {"id": "obs3", "species": "lion", ...}1# Endangered species
2GET /api/species?endangered=true
3
4# Species in savanna with population > 100
5GET /api/species?habitat=savanna&min_population=100
6
7# Observations within a date range
8GET /api/observations?start_date=2024-01-01&end_date=2024-01-311# Sort by population ascending
2GET /api/species?sort=population
3
4# Sort by population descending
5GET /api/species?sort=-population
6
7# Sort by name, then population
8GET /api/species?sort=name,population1# Page 1, 20 results per page
2GET /api/species?page=1&per_page=20
3
4# Alternative: offset and limit
5GET /api/species?offset=0&limit=20
6
7# Response with pagination metadata:
8{
9 "data": [...],
10 "pagination": {
11 "current_page": 1,
12 "per_page": 20,
13 "total_items": 150,
14 "total_pages": 8
15 }
16}1# Only name and population
2GET /api/species?fields=name,population
3
4Response: [
5 {"name": "Lion", "population": 120},
6 {"name": "Elephant", "population": 450}
7]1{
2 "id": "lion",
3 "scientific_name": "Panthera leo",
4 "common_name": "Lion",
5 "population": 120,
6 "habitat": "savanna",
7 "endangered": true,
8 "created_at": "2024-01-01T10:00:00Z",
9 "updated_at": "2024-01-15T14:30:00Z"
10}1{
2 "data": [
3 {"id": "lion", "name": "Lion", "population": 120},
4 {"id": "elephant", "name": "Elephant", "population": 450}
5 ],
6 "pagination": {
7 "page": 1,
8 "per_page": 20,
9 "total": 45,
10 "pages": 3
11 },
12 "links": {
13 "self": "/api/species?page=1",
14 "next": "/api/species?page=2",
15 "last": "/api/species?page=3"
16 }
17}1{
2 "error": {
3 "code": "SPECIES_NOT_FOUND",
4 "message": "Species 'unicorn' does not exist",
5 "status": 404,
6 "timestamp": "2024-01-15T14:30:00Z"
7 }
8}1{
2 "error": {
3 "code": "VALIDATION_ERROR",
4 "message": "Data validation errors",
5 "status": 400,
6 "details": [
7 {"field": "population", "message": "Must be a number greater than 0"},
8 {"field": "habitat", "message": "Field required"}
9 ]
10 }
11}Location header)1GET /api/species
2X-API-Key: your_api_key_here1GET /api/species
2Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...1GET /api/species
2Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=Most common: Bearer Token (OAuth 2.0, JWT)
1import requests
2from typing import List, Dict, Optional, Any
3from datetime import datetime
4
5class SafariRESTClient:
6 """REST API client for Safari Data API"""
7
8 def __init__(self, base_url: str, api_key: str):
9 self.base_url = base_url.rstrip("/")
10 self.session = requests.Session()
11 self.session.headers.update({
12 "Authorization": f"Bearer {api_key}",
13 "Accept": "application/json",
14 "Content-Type": "application/json"
15 })
16
17 # === SPECIES - /api/species ===
18
19 def list_species(self, habitat: Optional[str] = None,
20 endangered: Optional[bool] = None,
21 page: int = 1, per_page: int = 20,
22 sort: str = "name") -> Dict[str, Any]:
23 """
24 GET /api/species
25 List species with filtering, sorting, pagination
26 """
27 params = {
28 "page": page,
29 "per_page": per_page,
30 "sort": sort
31 }
32 if habitat:
33 params["habitat"] = habitat
34 if endangered is not None:
35 params["endangered"] = str(endangered).lower()
36
37 response = self.session.get(f"{self.base_url}/api/species", params=params)
38 response.raise_for_status()
39 return response.json()
40
41 def get_species(self, species_id: str) -> Dict[str, Any]:
42 """
43 GET /api/species/{id}
44 Fetch a single species
45 """
46 response = self.session.get(f"{self.base_url}/api/species/{species_id}")
47 response.raise_for_status()
48 return response.json()
49
50 def create_species(self, data: Dict[str, Any]) -> Dict[str, Any]:
51 """
52 POST /api/species
53 Create a new species
54 """
55 response = self.session.post(f"{self.base_url}/api/species", json=data)
56 response.raise_for_status()
57
58 if response.status_code == 201:
59 # Return created resource + ID from Location header
60 location = response.headers.get("Location")
61 print(f"Created: {location}")
62
63 return response.json()
64
65 def update_species(self, species_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
66 """
67 PUT /api/species/{id}
68 Replace entire species
69 """
70 response = self.session.put(
71 f"{self.base_url}/api/species/{species_id}",
72 json=data
73 )
74 response.raise_for_status()
75 return response.json()
76
77 def partial_update_species(self, species_id: str,
78 data: Dict[str, Any]) -> Dict[str, Any]:
79 """
80 PATCH /api/species/{id}
81 Update part of a species
82 """
83 response = self.session.patch(
84 f"{self.base_url}/api/species/{species_id}",
85 json=data
86 )
87 response.raise_for_status()
88 return response.json()
89
90 def delete_species(self, species_id: str) -> bool:
91 """
92 DELETE /api/species/{id}
93 Delete a species
94 """
95 response = self.session.delete(f"{self.base_url}/api/species/{species_id}")
96 response.raise_for_status()
97 return response.status_code == 204
98
99 # === OBSERVATIONS - /api/species/{id}/observations ===
100
101 def list_observations(self, species_id: str,
102 start_date: Optional[str] = None,
103 end_date: Optional[str] = None,
104 page: int = 1) -> Dict[str, Any]:
105 """
106 GET /api/species/{id}/observations
107 List observations for a species
108 """
109 params = {"page": page}
110 if start_date:
111 params["start_date"] = start_date
112 if end_date:
113 params["end_date"] = end_date
114
115 response = self.session.get(
116 f"{self.base_url}/api/species/{species_id}/observations",
117 params=params
118 )
119 response.raise_for_status()
120 return response.json()
121
122 def create_observation(self, species_id: str,
123 data: Dict[str, Any]) -> Dict[str, Any]:
124 """
125 POST /api/species/{id}/observations
126 Add an observation for a species
127 """
128 response = self.session.post(
129 f"{self.base_url}/api/species/{species_id}/observations",
130 json=data
131 )
132 response.raise_for_status()
133 return response.json()
134
135 def get_observation(self, species_id: str, observation_id: str) -> Dict[str, Any]:
136 """
137 GET /api/species/{id}/observations/{obs_id}
138 Fetch a specific observation
139 """
140 response = self.session.get(
141 f"{self.base_url}/api/species/{species_id}/observations/{observation_id}"
142 )
143 response.raise_for_status()
144 return response.json()
145
146 # === STATISTICS - /api/statistics ===
147
148 def get_statistics(self, group_by: str = "habitat") -> Dict[str, Any]:
149 """
150 GET /api/statistics
151 Fetch statistics
152 """
153 params = {"group_by": group_by}
154 response = self.session.get(
155 f"{self.base_url}/api/statistics",
156 params=params
157 )
158 response.raise_for_status()
159 return response.json()
160
161 def close(self):
162 """Close session"""
163 self.session.close()
164
165
166# === DEMONSTRATION ===
167
168print("=== SAFARI REST API CLIENT ===\n")
169
170client = SafariRESTClient(
171 base_url="https://api.safari-data.org",
172 api_key="demo_bearer_token_12345"
173)
174
175# 1. LIST - fetch all endangered species in savanna
176print("1. Species list (filtered, sorted, paginated)")
177result = client.list_species(
178 habitat="savanna",
179 endangered=True,
180 page=1,
181 per_page=10,
182 sort="-population" # Descending by population
183)
184
185print(f" Found: {result['pagination']['total']} species")
186for species in result['data']:
187 print(f" - {species['common_name']}: {species['population']} individuals")
188
189# 2. GET - fetch a specific species
190print("\n2. Fetching a specific species")
191lion = client.get_species("lion")
192print(f" {lion['common_name']} ({lion['scientific_name']})")
193print(f" Population: {lion['population']}")
194print(f" Endangered: {'Yes' if lion['endangered'] else 'No'}")
195
196# 3. POST - create new species
197print("\n3. Creating a new species")
198new_species = {
199 "scientific_name": "Acinonyx jubatus",
200 "common_name": "Cheetah",
201 "population": 7100,
202 "habitat": "savanna",
203 "endangered": True
204}
205
206created = client.create_species(new_species)
207print(f" Created: {created['common_name']} (ID: {created['id']})")
208
209# 4. PATCH - update only population
210print("\n4. Partial update (PATCH)")
211updated = client.partial_update_species("lion", {"population": 125})
212print(f" Updated population: {updated['population']}")
213
214# 5. Nested observations - list
215print("\n5. List of lion observations")
216observations = client.list_observations(
217 "lion",
218 start_date="2024-01-01",
219 end_date="2024-01-31"
220)
221
222print(f" Found {len(observations['data'])} observations in January")
223for obs in observations['data']:
224 print(f" - {obs['date']}: {obs['count']} individuals in {obs['location']}")
225
226# 6. Nested observations - create
227print("\n6. Adding a new observation")
228new_obs = {
229 "date": datetime.now().isoformat(),
230 "location": "Serengeti North",
231 "count": 15,
232 "notes": "Pride with 3 cubs"
233}
234
235obs_created = client.create_observation("lion", new_obs)
236print(f" Created observation ID: {obs_created['id']}")
237
238# 7. Statistics
239print("\n7. Fetching statistics")
240stats = client.get_statistics(group_by="habitat")
241print(" Statistics by habitat:")
242for habitat_stat in stats['data']:
243 print(f" - {habitat_stat['habitat']}: {habitat_stat['species_count']} species")
244
245# 8. DELETE - delete species
246print("\n8. Deleting a species")
247if client.delete_species("test-species"):
248 print(" Species deleted")
249
250client.close()
251print("\nDemonstration complete")Good:
/api/species, /api/observations
Bad: /api/specie, /api/observationGood:
/api/v1/species
Bad: /api/species (what when you change the format?)Good:
/species/lion/observations
Bad: /continents/africa/countries/kenya/parks/serengeti/species/lion/observationsGood:
{"scientific_name": "..."} or {"scientificName": "..."}
Bad: {"scientific_name": "...", "commonName": "..."} - mixed!Good:
GET /species?habitat=savanna&sort=-population&page=2
Bad: POST /species/filter with body1POST /api/species
2Response: 201 Created
3Location: /api/species/cheetah1Response Headers:
2X-RateLimit-Limit: 1000
3X-RateLimit-Remaining: 999
4X-RateLimit-Reset: 1640000000In this lesson you learned:
/api/v1/resources/:idBefore moving on:
POST /species and GET /species/{id}/species/{id}/observations)Safari Analogy: REST API is like universal communication standards between research stations worldwide. Thanks to them, a biologist from Poland knows that
GET /api/species/lion will return lion data - regardless of who built the API!In the next lesson, Darwin will teach you web scraping - how to collect data from websites that don't have an API!