We use cookies to enhance your experience on the site
CodeWorlds

REST API - data exchange architecture

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/lion

What is REST API?

REST (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

Why REST?

Without standards, every API would look different:

  • /getLion?id=123
  • /fetchLionData
  • /lion-information/get
  • /api/v1/animals/lion/retrieve

With REST everyone knows:

  • GET /api/species/lion
    - fetch a lion
  • POST /api/species
    - create a species
  • DELETE /api/species/lion
    - delete a lion

Predictable, consistent, intuitive!

REST Principles

1. Resources - nouns, not verbs

In REST everything is a resource - an object, entity, or data.

Good - nouns:

  • /species
    - species
  • /observations
    - observations
  • /habitats
    - habitats
  • /researchers
    - researchers

Bad - verbs:

  • /getSpecies
  • /createObservation
  • /deleteHabitat

The action is determined by the HTTP method, not the URL!

2. HTTP methods = actions on resources

| 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 |

3. Stateless

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 abc123

The server doesn't know "who sent this earlier" - each request is self-contained!

4. Resource hierarchy

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 Serengeti

Rule:

/resource/id/subresource/id

5. API versioning

APIs evolve - versioning prevents breaking existing clients:

1/api/v1/species
2/api/v2/species

Change version when:

  • You remove fields from responses
  • You change the data format
  • You remove endpoints

RESTful Endpoints - standard patterns

CRUD (Create, Read, Update, Delete) for a resource

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)

Nested resources

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", ...}

Query Parameters - filtering, sorting, pagination

Filtering

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-31

Sorting

1# 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,population

Pagination

1# 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}

Field selection

1# Only name and population
2GET /api/species?fields=name,population
3
4Response: [
5  {"name": "Lion", "population": 120},
6  {"name": "Elephant", "population": 450}
7]

JSON Response Structure

Success - single resource

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}

Success - resource list with pagination

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}

Error - standard format

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}

Validation errors

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}

HTTP Status Codes in REST

2xx - Success

  • 200 OK - GET, PUT, PATCH - success with data
  • 201 Created - POST - resource created (+
    Location
    header)
  • 204 No Content - DELETE - success without data

4xx - Client Error

  • 400 Bad Request - invalid data (e.g., bad JSON, validation)
  • 401 Unauthorized - no authentication
  • 403 Forbidden - no permissions (authenticated, but no access)
  • 404 Not Found - resource doesn't exist
  • 409 Conflict - conflict (e.g., duplicate ID)
  • 422 Unprocessable Entity - valid format, but logically incorrect data
  • 429 Too Many Requests - rate limit exceeded

5xx - Server Error

  • 500 Internal Server Error - server error
  • 503 Service Unavailable - service unavailable

Authentication and Authorization

API Key

1GET /api/species
2X-API-Key: your_api_key_here

Bearer Token (JWT)

1GET /api/species
2Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Basic Auth

1GET /api/species
2Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

Most common: Bearer Token (OAuth 2.0, JWT)

Safari example - full REST API client

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")

REST API Best Practices

1. Use plural nouns

Good:

/api/species
,
/api/observations
Bad:
/api/specie
,
/api/observation

2. Always version your API

Good:

/api/v1/species
Bad:
/api/species
(what when you change the format?)

3. Resource hierarchy no deeper than 2 levels

Good:

/species/lion/observations
Bad:
/continents/africa/countries/kenya/parks/serengeti/species/lion/observations

4. Use snake_case or camelCase - consistently

Good:

{"scientific_name": "..."}
or
{"scientificName": "..."}
Bad:
{"scientific_name": "...", "commonName": "..."}
- mixed!

5. Filters, sorting, pagination via query params

Good:

GET /species?habitat=savanna&sort=-population&page=2
Bad:
POST /species/filter
with body

6. Return appropriate status codes

  • 201 Created (not 200) for POST
  • 204 No Content for DELETE
  • 404 Not Found when resource doesn't exist
  • 400 Bad Request for validation errors

7. Use the Location header for new resources

1POST /api/species
2Response: 201 Created
3Location: /api/species/cheetah

8. Rate limiting - communicate limits

1Response Headers:
2X-RateLimit-Limit: 1000
3X-RateLimit-Remaining: 999
4X-RateLimit-Reset: 1640000000

Summary

In this lesson you learned:

  • What REST API is and what it's for
  • REST principles: resources, HTTP methods, stateless, hierarchy
  • RESTful endpoints:
    /api/v1/resources/:id
  • CRUD operations: GET, POST, PUT, PATCH, DELETE
  • Query parameters: filtering, sorting, pagination
  • JSON response structure (data, pagination, error)
  • Status codes in the context of REST
  • Authentication: API Key, Bearer Token
  • REST API best practices

Checkpoint

Before moving on:

  • [ ] You understand the difference between
    POST /species
    and
    GET /species/{id}
  • [ ] You can design a RESTful endpoint
  • [ ] You know how to use query params for filtering
  • [ ] You understand nested resources (
    /species/{id}/observations
    )
  • [ ] You know REST API best practices

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!

Go to CodeWorlds