READ: Obtain UUID of the city and get the city
July 17, 2025 · View on GitHub
Table of Contents | ← Web Protocols | GraphQL →
REST
The following is the supplementary content to the REST chapter. The examples in this chapter show how to work with the WFS' REST APIs.
Requirements
Lab Setup
The following steps create three Docker containers: django-app, django-postgres, and django-redis.
These steps need to be executed only once.
cd src/django
docker compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g)
docker compose up --detach --wait
Visit app at http://localhost:8000
Important
When working with GitHub Codespaces, you'll use a unique URL containing the Codespace's name instead of the localhost URL.
Implementation
This section demonstrates how to implement a REST API using Django REST Framework. The implementation follows the API design patterns described in the API Design Patterns chapter. To execute the code examples, first set up the lab environment, as shown in the lab setup.
Implementing Update from CRUD
The book covers the implementation of Create, Read, and Delete operations.
This section shows how to implement the Update operation, particularly partial update and complete update operations.
The partial update allows the client to send only a portion of the object it wishes to modify, while a complete update requires the entire object to be passed, which replaces the updated resource.
Partial Update
The following code shows how to implement the partial update operation.
This operation is called partial update because the client in the request body sends only a part of the object it wishes to modify.
The operation uses the HTTP PATCH method.
# src/django/app/core/api/rest/v1/views/city_views.py
...
class CityView(GenericAPIView):
...
def patch(self, request, uuid): # <1>
try:
city = CityRepository.get_by_id(uuid) # <2>
except Exception:
errors = [f"Failed to read from City database."]
return error_response(errors, 500)
if not city: # <3>
errors = [f"Referenced City ({uuid}) not found."]
return error_response(errors, 404)
serializer = self.get_serializer(city, data=request.data, partial=True) # <4>
if not serializer.is_valid(): # <5>
errors = get_serializer_errors(serializer)
return error_response(errors, 400)
try:
serializer.save() # <6>
except Exception:
errors = [f"Failed to save City ({uuid}) object."]
return error_response(errors, 500) # <6>
return success_response(serializer.data, 200) # <6>
...
-
The
patch()method handles the HTTPPATCHrequest. The method argumentuuidis used to identify the resource. -
If a database read fails while retrieving the record (resource), the HTTP response with a 500 (Internal Server Error) status code is returned.
-
If the city doesn't exist, the HTTP response with a 404 (Not Found) status code is returned.
-
A serializer is created from the
cityobject and request data. Thepartial=Truekeyword argument allows the client to update a portion of the city data. -
The request's data is validated; if the data is invalid, the HTTP response with a 400 (Bad Request) status code is returned.
-
If the saving data operation fails, the HTTP response with a 500 (Internal Server Error) status code is returned, indicating that something went wrong with the database write. If the operation succeeds, the HTTP response containing the serialized city data with a 200 (OK) status code is returned.
The following code shows the curl client making a partial resource update with an HTTP PATCH request.
Important
The UUID of the city will be different in your environment.
CREDENTIALS_PAYLOAD='{"username":"admin","password":"admin"}'
ACCESS_TOKEN=$(docker compose exec app bash -c \
"curl \
--data '$CREDENTIALS_PAYLOAD' \
--header 'Content-Type: application/json' \
--request 'POST' \
--silent 'http://localhost:8000/api/jwt/obtain' | \
jq --raw-output '.access'")
CITY_UUID=$(docker compose exec app bash -c \
"curl --request 'GET' --silent 'http://localhost:8000/api/cities?search_name=Tokyo' | \
jq --raw-output '.results[0].uuid'")
CITY_UPDATE_PAYLOAD='{"name":"Tokio"}'
docker compose exec app bash -c \
"curl \
--data '$CITY_UPDATE_PAYLOAD' \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--request PATCH \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID' | \
jq"
{
"results": [
{
"uuid": "07608d6d-6a26-4f43-8e96-e3e18cb50591",
"name": "Tokio",
"country": "Japan",
"region": "Asia",
"timezone": "Asia/Tokyo",
"latitude": "35.689500",
"longitude": "139.691700",
"deleted": false
}
]
}
Complete Update
The following code shows how to implement the complete update operation.
This operation performs a complete update (full update).
The client sends the entire object in the request body, which replaces the updated resource.
The complete update uses the HTTP PUT method.
# src/django/app/core/api/rest/v1/views/city_views.py
class CityView(GenericAPIView):
...
def put(self, request, uuid): # <1>
try:
city = CityRepository.get_by_id(uuid) # <2>
except Exception:
errors = [f"Failed to read from City database."]
return error_response(errors, 500)
if not city: # <3>
errors = [f"Referenced City ({uuid}) not found."]
return error_response(errors, 404)
serializer = self.get_serializer(city, data=request.data) # <4>
if not serializer.is_valid(): # <5>
errors = get_serializer_errors(serializer)
return error_response(errors, 400)
try:
serializer.save() # <6>
except Exception:
errors = [f"Failed to update City ({uuid}) object."]
return error_response(error, 500) # <6>
return success_response(serializer.data, 200) # <6>
...
-
The
put()method handles an HTTPPUTrequest. The method argumentuuidis used to identify the resource. -
If a database read fails while retrieving the record (resource), the HTTP response with a 500 (Internal Server Error) status code is returned.
-
If the city doesn't exist, the HTTP response with a 404 (Not Found) status code is returned.
-
A serializer is created from the
cityobject and request data. -
If the request data is invalid, the HTTP response with a 400 (Bad Request) status code is returned.
-
The city resource is updated by calling the
save()method on the serializer. Unsuccessful operation assumes that the database write didn't happen, and the HTTP response with a 500 (Internal Server Error) status code is returned. If the operation succeeds, the HTTP response containing the updated data with a 200 (OK) status code is returned.
The following command shows the curl client making a complete resource update with an HTTP PUT request.
Important
The UUID of the city will be different in your environment.
CREDENTIALS_PAYLOAD='{"username":"admin","password":"admin"}'
ACCESS_TOKEN=$(docker compose exec app bash -c \
"curl \
--data '$CREDENTIALS_PAYLOAD' \
--header 'Content-Type: application/json' \
--request 'POST' \
--silent 'http://localhost:8000/api/jwt/obtain' | \
jq --raw-output '.access'")
CITY_UUID=$(docker compose exec app bash -c \
"curl --request 'GET' --silent 'http://localhost:8000/api/cities?search_name=Istanbul' | \
jq --raw-output '.results[0].uuid'")
CITY_UPDATE_PAYLOAD='{"name": "Ankara", "country": "Turkey", "region": "Asia", "timezone": "Europe/Istanbul", "latitude": "39.9334", "longitude": "32.8597"}'
docker compose exec app bash -c \
"curl \
--data '$CITY_UPDATE_PAYLOAD' \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--request PUT \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID' | \
jq"
{
"results": [
{
"uuid": "7941b0df-ccf5-48bd-a525-5392170f5117",
"name": "Ankara",
"country": "Turkey",
"region": "Asia",
"timezone": "Europe/Istanbul",
"latitude": "39.933400",
"longitude": "32.859700",
"deleted": false
}
]
}
Security
This section shows how to obtain JSON Web Tokens (JWTs), which are used to authenticate and authorize the user when interacting with protected endpoints of the WFS REST API. Additionally, we describe how to enable Transport Layer Security (TLS) for the WFS REST API.
Authentication and Authorization (JWT)
The example below shows how to obtain, verify, refresh, and revoke the JWT token. The JWT is implemented using the Simple JWT plugin for the Django REST framework.
# Obtain JWT tokens
CREDENTIALS_PAYLOAD='{"username":"admin","password":"admin"}'
ACCESS_TOKEN=$(docker compose exec app bash -c \
"curl \
--data '$CREDENTIALS_PAYLOAD' \
--header 'Content-Type: application/json' \
--request 'POST' \
--silent 'http://localhost:8000/api/jwt/obtain' | \
jq --raw-output '.access'")
REFRESH_TOKEN=$(docker compose exec app bash -c \
"curl \
--data '$CREDENTIALS_PAYLOAD' \
--header 'Content-Type: application/json' \
--request POST \
--silent \
'http://localhost:8000/api/jwt/obtain' | \
jq --raw-output '.refresh'")
echo $ACCESS_TOKEN
echo $REFRESH_TOKEN
# Verify JWT token
docker compose exec app bash -c \
"curl \
--data '{\"token\": \"$ACCESS_TOKEN\"}' \
--header 'Content-Type: application/json' \
--request POST \
--silent \
--output /dev/null \
--write-out '%{http_code}\n' \
'http://localhost:8000/api/jwt/verify' \
jq"
# Refresh JWT token
docker compose exec app bash -c \
"curl \
--data '{\"refresh\":\"$REFRESH_TOKEN\"}' \
--header 'Content-Type: application/json' \
--request POST \
--silent \
'http://localhost:8000/api/jwt/refresh' | \
jq"
# Revoke JWT token
docker compose exec app bash -c \
"curl \
--data '{\"refresh\":\"$REFRESH_TOKEN\"}' \
--header 'Content-Type: application/json' \
--output /dev/null \
--request POST \
--silent \
--write-out '%{http_code}\n' \
'http://localhost:8000/api/jwt/revoke' | \
jq"
Transport Layer Security
Note
When working with GitHub Codespaces, you don't need to enable TLS. The Codespaces environment is already secured with TLS.
This example shows how to enable TLS for the WFS REST API.
It requires changing an environment variable in the src/django/compose.yaml file and restarting the Docker containers.
The TLS is enabled by setting the TLS_ENABLE environment variable to 1.
The default value is 0, which disables TLS.
# Enable TLS
sed --in-place 's/- TLS_ENABLE=0/- TLS_ENABLE=1/g' compose.yaml
# Restart containers
docker compose up --detach --wait
The following code shows a fragment of a start-up script that generates a self-signed SSL certificate.
# src/django/app/scripts/startup.sh
...
if [ "$TLS_ENABLE" -eq 1 ]; then # <1>
UVICORN_OPTIONS_TLS="
--ssl-certfile /etc/wfs/ssl/certs/wfs.crt \
--ssl-keyfile /etc/wfs/ssl/private/wfs.key
" # <2>
...
openssl req \
-config /usr/src/config/wfs.ssl.conf \
-keyout /etc/wfs/ssl/private/wfs.key \
-newkey rsa:4096 \
-out /etc/wfs/ssl/certs/wfs.crt \
-nodes \
-x509 # <3>
...
uvicorn config.asgi:application_django ... $UVICORN_OPTIONS_TLS # <4>
...
-
The
TLS_ENABLEenvironment controls the SSL certification creation and secure startup of the Django application that exposes the REST API. -
The
UVICORN_OPTIONS_TLSenvironment variable contains flags used by the uvicorn asynchronous server gateway interface (ASGI) to secure the traffic. -
The
opensslprogram generates a self-signed SSL certificate from src/django/app/config/wfs.ssl.conf configuration described below in [chapter-rest-ssl-config]. -
The
uvicornprogram starts the Django application using certificates provided in theUVICORN_OPTIONS_TLSenvironment variable. The generated certificates are then passed to the Django (and Starlette) applications that expose APIs.
[req] # <1>
default_bits = 4096
distinguished_name = req_distinguished_name
prompt = no
req_extensions = req_ext
[ca] # <2>
default_ca = CA_default
[CA_default] # <3>
default_crl_days = 30
default_days = 365
default_md = sha1
[req_distinguished_name] # <4>
commonName = localhost
[req_ext] # <5>
subjectAltName = @alt_names
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature
extendedKeyUsage = serverAuth
[alt_names] # <6>
IP.1 = 127.0.0.1
-
The
reqsection defines settings for certificate requests. -
The
casection defines the certificate authority (CA) settings. -
The
CA_defaultsection defines the default settings for the CA. -
The
req_distinguished_namesection defines the certificate's distinguished name (DN), thecommonNamefield (see Moxie Marlinspike's talk More Tricks for Defeating SSL from Def Con 17). Notice that values are set tolocalhost, indicating that the certificate is to work only in the local environment. -
The
req_extsection defines several extensions for the certificate generation. ThesubjectAltNamesubject alternative names (SAN) for the certificate points to a list of additional domain names (@alt_names) or IP addresses that the certificate will secure. ThebasicConstraintsestablishes constraints of the certificate. TheCA:FALSEspecifies that the certificate is not a certificate authority, meaning it cannot issue or sign other certificates. ThekeyUsagedefines the purposes for which the certificate's public key can be used. ThedigitalSignaturepermits the key to verify digital signatures. TheextendedKeyUsagerefines the certificate's intended use.serverAuthdesignates the certificate for authenticating servers, making it suitable for TLS/SSL protocols used in securing web servers and other services. Thecriticalflag indicates that this extension are to be recognized and enforced by any certificate-using application. -
This section defines SANs for the certificate. In this case, it's the loopback IP address, also known as localhost.
Warning
When visiting https://localhost:8000/, the browser will display a warning that your connection is not private. To secure connection in the local environment add the application's SSL certificate to the browser's exception list. To do it visit https://localhost:8000 (for HTTP traffic), accept the risk, and continue to the webpage.
Now application traffic is secured which can be confirmed by visiting https://localhost:8000
Documentation
The example below shows how to convert the OpenAPI schema (described in the YAML file) to the API documentation using the Swagger UI.
# Get OpenAPI schema
docker compose exec app bash -c \
"curl --output schema.yaml --silent http://localhost:8000/api/docs/v1/schema/manual"
# Run Swagger UI
docker run \
--env SWAGGER_JSON_URL=schema.yaml \
--publish 127.0.0.1:8888:8080 \
--rm \
--volume ${PWD}/app/schema.yaml:/usr/share/nginx/html/schema.yaml \
swaggerapi/swagger-ui
The generated documentation is available at http://localhost:8888
Important
When working with GitHub Codespaces, you'll use a unique URL containing the Codespace's name instead of the localhost URL.
Testing
The command below shows how to test the WFS APIs. The commands run BDD tests and unit tests.
# Run tests
docker compose exec app python manage.py behave --no-input
docker compose exec app python manage.py test --no-input
# Run tests with coverage
docker compose exec app coverage run manage.py test --no-input
docker compose exec app coverage report --skip-covered --show-missing --sort cover
CRUD
The following example shows how to perform Create, Read, Update, and Delete actions on the city resources with the REST API.
# Get JWT access token
CREDENTIALS_PAYLOAD='{"username":"admin","password":"admin"}'
ACCESS_TOKEN=$(docker compose exec app bash -c \
"curl \
--data '$CREDENTIALS_PAYLOAD' \
--header 'Content-Type: application/json' \
--request 'POST' \
--silent 'http://localhost:8000/api/jwt/obtain' | \
jq --raw-output '.access'")
# CREATE: Create a city
CREATE_CITY_PAYLOAD='{"name":"Copenhagen",
"country":"Denmark",
"region":"Europe",
"timezone":"Europe/Copenhagen",
"latitude":55.676100,
"longitude":12.568300}'
docker compose exec app bash -c \
"curl \
--data '$CREATE_CITY_PAYLOAD' \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--request 'POST' \
--silent \
'http://localhost:8000/api/cities' | \
jq"
# READ: Obtain UUID of the city and get the city
CITY_UUID=$(docker compose exec app bash -c \
"curl --request 'GET' --silent 'http://localhost:8000/api/cities?search_name=Copenhagen' | \
jq --raw-output '.results[0].uuid'")
docker compose exec app bash -c \
"curl \
--request 'GET' \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID' | \
jq"
# UPDATE: Perform a partial city update
UPDATE_CITY_PAYLOAD='{"name":"København","country":"Danmark"}'
docker compose exec app bash -c \
"curl \
--data '$UPDATE_CITY_PAYLOAD' \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--request 'PATCH' \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID' | \
jq"
# UPDATE: Perform a full city update
UPDATE_CITY_PAYLOAD='{"name":"Copenhagen",
"country":"Denmark",
"region":"Europe",
"timezone":"Europe/Copenhagen",
"latitude":55.676100,
"longitude":12.568300}'
docker compose exec app bash -c \
"curl \
--data '$UPDATE_CITY_PAYLOAD' \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--request 'PUT' \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID' | \
jq"
# DELETE: Remove the city
docker compose exec app bash -c \
"curl \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--request 'DELETE' \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID?soft_delete#true' | \
jq"
Pagination
This example shows how to paginate over the REST API.
# First page
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/cities?page=1&page_size=1' | \
jq"
# Second page
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/cities?page=2&page_size=1' | \
jq"
# Last page
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/cities?page=6&page_size=1' | \
jq"
Searching, Sorting, Filtering, and Counting
The following example shows how to search, sort, and filter datasets using the REST API.
Note that the fields query parameter is used to control the fields returned in the response payload.
# Searching
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/cities?search_name=Mexico' | \
jq"
# Sorting
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/cities?sort=-name' | \
jq"
# Filtering and Counting
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/cities?page_size=10&sort=-name&fields=uuid,name' | \
jq"
Soft-delete and Hard-delete
The following example shows how to perform soft-delete and hard-delete operations on the city resources. The soft-delete operation marks the resource as deleted, while the hard-delete operation removes it from the database.
CITY_UUID=$(docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/cities?search_name=Sao%20Paulo' | \
jq --raw-output '.results[0].uuid'")
CREDENTIALS_PAYLOAD='{"username":"admin","password":"admin"}'
ACCESS_TOKEN=$(docker compose exec app bash -c \
"curl \
--data '$CREDENTIALS_PAYLOAD' \
--header 'Content-Type: application/json' \
--request 'POST' \
--silent 'http://localhost:8000/api/jwt/obtain' | \
jq --raw-output '.access'")
# Soft-delete the city
docker compose exec app bash -c \
"curl \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--request DELETE \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID?soft_delete=true' | \
jq"
# Soft-delete the city again
docker compose exec app bash -c \
"curl \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--request DELETE \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID?soft_delete=true' | \
jq"
# Get soft-deleted city
docker compose exec app bash -c \
"curl \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID' | \
jq"
# Hard-delete the city
docker compose exec app bash -c \
"curl \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--request DELETE \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID?soft_delete=false' | \
jq"
# Hard-delete the city again
docker compose exec app bash -c \
"curl \
--header 'Authorization: Bearer $ACCESS_TOKEN' \
--request DELETE \
--silent \
'http://localhost:8000/api/cities/$CITY_UUID?soft_delete=false' | \
jq"
Caching
This example shows the effect of response server-side caching. Note the difference in latency between two consecutive response times.
APIs
This section contains descriptions of API endpoints not covered in the book.
Geocoding API
The geocoding API allows you to convert city names into geographic coordinates (latitude and longitude) and vice versa.
# Test geocoding API
docker compose exec app bash -c \
"curl --silent http://localhost:8000/api/geocoding?city=Sydney | \
jq"
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/geocoding?lat=55.675966&lon=12.569117' | \
jq"
Seed API
The seed API lets you populate weather data for a given city and year.
# Get UUID of the first city
CITY_UUID=$(curl --silent http://localhost:8000/api/cities | jq --raw-output '.results[0].uuid')
ACCESS_TOKEN=$(docker compose exec app bash -c \
"curl \
--data '$CREDENTIALS_PAYLOAD' \
--header 'Content-Type: application/json' \
--request 'POST' \
--silent 'http://localhost:8000/api/jwt/obtain' | \
jq --raw-output '.access'")
# Test seed API - remember to update the year to the current year
docker compose exec app bash -c \
"curl \
--data '{\"year\": 2025, \"city_uuid\": \"$CITY_UUID\"}' \
--header \"Authorization: Bearer $ACCESS_TOKEN\" \
--header \"Content-Type: application/json\" \
--output /dev/null \
--request PUT \
--silent \
--write-out \"%{http_code}\n\" \
'http://localhost:8000/api/seed'"
Weather Forecasts and History APIs
The weather forecasts and history APIs allow you to retrieve weather data for a given city and date range.
# Get UUID of the first city
CITY_UUID=$(docker compose exec app bash -c \
"curl --silent http://localhost:8000/api/cities | \
jq --raw-output '.results[0].uuid'")
# Test forecasts and history API
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/forecasts?city_uuid=$CITY_UUID&days=2' | \
jq"
docker compose exec app bash -c \
"curl --silent 'http://localhost:8000/api/history?city_uuid=$CITY_UUID&start_date=1940-01-01&end_date=2023-08-12' | \
jq"
Migrations
This section contains commands used to set up the database and populate it with data.
docker compose exec app python manage.py makemigrations
docker compose exec app python manage.py migrate
docker compose exec app python manage.py app_seed
Lab teardown
After the lab from this chapter is no longer needed, the containers can be stopped and removed.
docker compose down









