MAD Study Hub

Home / MAD1 / Week 6

Week 6 · APIs and REST: OpenAPI and Flask-RESTful

Official syllabus line: APIs and REST APIs
About 120 minQuiz 2 material (see exam radar)BSCS2003 (+ project BSCS2003P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD1 week 4 (models) and week 5 (Flask routes, request object). Basic idea of JSON as Python dicts and lists.

The one-minute story

Think of it like this

An API is a restaurant menu plus a waiter. You do not walk into the kitchen. You read the menu (the documentation), tell the waiter "one dosa, please" (an HTTP request), and the waiter brings the dish (the response) without showing you how it was cooked. A REST API is a menu with strict, simple manners: every dish has its own name (URL), every action has a standard word (GET to look, POST to order, PUT to change the order, DELETE to cancel), and the waiter treats every call as new: he does not remember your last visit, so you carry what you need each time (stateless).

A normal web page route returns HTML for a person. An API route returns JSON for another program: a mobile app, a Vue page, or a test script.

By the end of this week you can:

  • explain what an API is and name the REST constraints;
  • design URLs and choose the right verb and status code for each action;
  • write a Flask-RESTful API with Resource, reqparse, marshal_with and abort;
  • predict the output of Flask-RESTful code and of curl commands (including which call gives an error);
  • document an API in OpenAPI (Swagger) YAML;
  • test an API with curl, Postman or Python requests.

Words you need

Words you need
  • API - Application Programming Interface: a defined way for one program to talk to another.
  • REST - a style for web APIs built on resources, URLs and standard HTTP verbs.
  • Resource - a thing the API exposes (a student, a book), named by a URL.
  • Endpoint - one URL plus one method (for example GET /api/books/3).
  • JSON - JavaScript Object Notation: the text format for API data (objects, lists, strings, numbers, true, false, null).
  • Stateless - the server keeps no memory of earlier requests; each request carries all it needs.
  • Idempotent - doing it once or many times leaves the server in the same state.
  • Status code - the three-digit result number (200, 404...).
  • OpenAPI - a standard format (in YAML or JSON) to describe an API; Swagger is the tooling around it.
  • Flask-RESTful - a Flask extension where each resource is a class with methods named after HTTP verbs.
  • reqparse - Flask-RESTful's helper to read and validate request data.
  • marshal / marshal_with - shape the response using a fields dictionary.
  • curl - a command-line program that sends HTTP requests.
  • Pagination - returning a long list in pages.

Idea 1: What an API is and what makes it REST

The server exposes data, and any client can use it: a browser page, an Android app, or a Python script. They agree on URLs, methods and JSON.

The idea

REST constraints (know each in one line):

  1. Client-server: UI and data storage are separate, so each can change on its own.
  2. Stateless: every request has everything the server needs (no server-side memory between calls).
  3. Cacheable: responses say whether they can be reused, so repeated reads are cheap.
  4. Uniform interface: resources identified by URLs, standard verbs (GET, POST, PUT, DELETE), representations such as JSON.
  5. Layered system: the client cannot tell if there is a proxy or load balancer between it and the server.

(A sixth, "code on demand", is optional.)

API route versus web page route. A web route returns HTML (render_template) and is meant for a person; an API route returns JSON and a status code, and its errors are JSON too (not a styled error page). In one real question the route / returned a Python dict and the answer was that its content type is application/json; and the route returning jsonify({"The list of gadgets": Gadgets}) was a valid API access point: both / and /home can be considered API endpoints.

JSON in 60 seconds

{
  "name": "Asha",
  "age": 27,
  "married": false,
  "phones": ["212-555-1234", 2125551234],
  "address": {"city": "Chennai", "pin": null}
}

Rules: double quotes only on keys and strings, no trailing comma, no comments, values are string, number, true, false, null, object or array. Python mapping: object is dict, array is list, true is True, null is None. In JavaScript, JSON.parse('{"name":"icecream","flavor":"vanilla","type":"cup"}') gives an object, and obj.name + ", " + obj.flavor + ", " + obj.type prints icecream, vanilla, cup: access is by key name, not by position.

Idea 2: Designing the API: URLs, verbs, status codes

Name resources with nouns, in plural, and put the action in the verb, not in the URL.

ActionMethod and URLSuccess status
list all booksGET /api/books200
read oneGET /api/books/3200 (404 if absent)
createPOST /api/books with a JSON body201 Created
replace/updatePUT /api/books/3 with a JSON body200 (or 204)
deleteDELETE /api/books/3200 or 204 No Content

Not RESTful: GET /api/deleteBook?id=3 or /getAllBooks. (A real exam question used URLs like /delete/val1 and a PUT request: because a route only answered PUT, a plain GET gave "none of these" = 405.)

The CRUD mapping: Create = POST, Read = GET, Update = PUT (or PATCH for a partial update), Delete = DELETE.

Safe and idempotent

MethodSafe (no change)Idempotent
GETyesyes
PUTnoyes (setting the same value twice gives the same state)
DELETEnoyes (the second delete changes nothing more; it may return 404)
POSTnono (two POSTs create two records)

Status codes for APIs

CodeMeaningTypical use
200OKsuccessful GET or PUT
201Createdsuccessful POST that made a record
204No Contentsuccess with no body (delete)
400Bad Requestmalformed or missing input
401Unauthorizednot logged in or bad credentials
403Forbiddenlogged in but not allowed
404Not Foundno such resource or URL
405Method Not Allowedthe URL exists but not for this verb
409Conflictduplicate, such as an existing username
422Unprocessableinput is well-formed but fails validation
500Server Errora bug in your code
Solved example: 1. Choose the verb and the code

Question. Match each action: (a) a new student is added; (b) a student's grade is changed; (c) a student that does not exist is requested; (d) someone adds a student whose roll number already exists; (e) a student is removed.

Solution. (a) POST /students, 201. (b) PUT /students/4, 200. (c) GET /students/99, 404. (d) POST /students, 409 Conflict (some courses use 400 or 422; say which you picked and why). (e) DELETE /students/4, 204 (or 200 with a message).

Watch out - real exam trap
  • Do not return 200 with {"error": ...} for a failure. Use the proper code so clients can react.
  • 401 is "who are you?", 403 is "I know you, but no".
  • A 404 for a missing resource and a 405 for a wrong verb are different problems.
  • POST is not idempotent; PUT and DELETE are.

Idea 3: Flask-RESTful: Api, Resource and methods

Install: pip install flask-restful. Each Resource is a class; its methods are named get, post, put, delete and are called by the verb. You register it with add_resource(Class, url...).

from flask import Flask, request
from flask_restful import Api, Resource

app = Flask(__name__)
api = Api(app)

students = {1: {"name": "Arjun", "grade": "A"}}

class Student(Resource):
    def get(self, student_id):
        if student_id in students:
            return students[student_id], 200
        return {"message": "Student not found"}, 404

    def put(self, student_id):
        data = request.json
        if student_id in students:
            students[student_id]["grade"] = data.get("grade", students[student_id]["grade"])
            return {"message": "Student record updated", "student": students[student_id]}, 200
        return {"message": "Student not found"}, 404

api.add_resource(Student, "/student/<int:student_id>")

I ran this with the test client. A GET /student/1 returns status 200 and {"name": "Arjun", "grade": "A"}. GET /student/10 returns 404 and {"message": "Student not found"}. PUT /student/1 with JSON {"grade": "A+"} returns 200 and the updated student (grade A+). PUT /student/4 returns 404 because 4 is not in the dictionary: it does not create a record. A URL like /student/abc does not match <int:...> and gives the plain 404 page.

What Flask-RESTful does for you: a returned dict or list becomes JSON; a returned tuple (body, status) sets the status; the response mimetype is application/json. Returning None gives JSON null with status 200.

The idea

Reading add_resource (very common question).

  • The URL variables must match the method's parameters by name. def get(self, tno) with "/api/ver1/<int:tno>" works; "/api/ver1/<tno>" gives a string tno, so train["train_no"] == tno (an int) would never be equal.
  • One class can have many URLs: api.add_resource(UserResource, "/users", "/users/<int:user_id>"). The same method is used for each; a method that has the wrong parameters for the URL fails with a 500 error.
  • A verb with no method in the class gives 405 Method Not Allowed.
  • Only the <...> parts bind variables. /api/<brand>/<model> matches /api/maruti/swift, but not /api/brand=Ford,model=Fiesta (one piece, one variable missing) and not /api/Honda&City.
Solved example: 2. Which curl command fails?

Question.

class UserResource(Resource):
    def get(self):
        return {"status": "GET request"}, 200
    def post(self, user_id):
        return {"status": "POST request", "user_id": user_id}, 201

api.add_resource(UserResource, "/users", "/users/<int:user_id>")

Which of these give an error: GET /users, GET /users/5001, POST /users, POST /users/5001?

Solution. I ran it. GET /users gives 200. POST /users/5001 gives 201 and {"status": "POST request", "user_id": 5001}. GET /users/5001 fails: Flask passes user_id=5001 to get(self), which takes no such argument: TypeError ... unexpected keyword argument, shown as 500. POST /users fails the same way: post(self, user_id) is called with nothing: TypeError ... missing 1 required positional argument. The two failing commands are GET /users/5001 and POST /users.

Solved example: 3. Methods and routes together

Question.

class MyApi(Resource):
    def get(self):  return {"greet": "Hello from GET Api!"}
    def put(self):  return {"greet": "Hello from PUT Api!"}

api.add_resource(MyApi, "/api/get", "/api/put", "/api/post")

Which of curl .../api/get -X get, -X put on /api/put, -X post on /api/post, -X put on /api/get, -X get on /api/put and -X get on /api/post throw an error?

Solution. The URL name means nothing: all three URLs go to the same class, whose methods are get and put. Only the verb matters. So everything works except POST (no post method), which gives 405: only -X post on /api/post fails. A lower-case -X get also works, because Flask-RESTful lower-cases the method name.

Idea 4: Input and output: reqparse, marshal_with, abort

reqparse declares which inputs you expect, converts their type and rejects bad input with a 400.

from flask_restful import reqparse

parser = reqparse.RequestParser()
parser.add_argument("n", type=int, required=True, help="n must be an int")

class MyResponse(Resource):
    def post(self):
        args = parser.parse_args()
        return args                      # a dict-like, so it becomes JSON

api.add_resource(MyResponse, "/")
curl -X POST http://127.0.0.1:5000 -H "Content-Type: application/json" -d "{\"n\":3}"

The reply is {"n": 3} (status 200). With {"n": "x"}, or with no n at all, the reply is 400 and {"message": {"n": "n must be an int"}}. If you send JSON with -d but leave out the Content-Type header, Flask 3 refuses it with a 415 error: so for JSON, curl needs -H "Content-Type: application/json" (as the real question's right answers had). Plain -d "status=active&role=admin" sends form-encoded data (not JSON); with -X PUT it is a PUT in form format, which was the right answer to a real question.

By default reqparse looks in the JSON body and the form. For values from the query string, say so: parser.add_argument("page", type=int, default=1, location="args"). (I found that on Flask 3 a GET with no JSON header and reqparse's default location raises 415, so always set location="args" for GET parameters.)

marshal_with filters and converts your output to the shape in a fields dictionary.

from flask_restful import fields, marshal_with

out_fields = {"Id": fields.Integer, "name": fields.String}

class MyApi(Resource):
    @marshal_with(out_fields)
    def post(self):
        info = parser.parse_args()     # parser has Id, name, email
        return info

curl -X POST ... -d '{"Id":3, "name":"user_1", "email":"user_1@gmail.com"}' returns {"Id": 3, "name": "user_1"}. The email is dropped because it is not in the fields dictionary, and "Id": 3 is a number (fields.Integer), not the string "3". A real question had the same code and its right option was {"Id": 3, "name": "user_1"}. The get method in that class returned a plain dict with the keys Identity, Name, E-mail: no marshal there, so keys are what you wrote.

marshal_with(fields, envelope="book") wraps the output as {"book": {...}}. Fields: fields.Integer, fields.String, fields.Float, fields.Boolean, fields.List(...), fields.Nested(...).

abort with a message. Flask-RESTful has its own abort.

from flask_restful import abort
abort(404, message="Book 99 not found")     # JSON: {"message": "Book 99 not found"}, status 404
Solved example: 4. A real CRUD API in one class

Question. Write the full API for books: list with paging, read one, create with validation.

Solution. Tested code:

from flask import Flask
from flask_restful import Api, Resource, reqparse, fields, marshal_with, marshal, abort

app = Flask(__name__)
api = Api(app)

ITEMS = [{"id": i, "title": f"Book {i}", "price": i * 10, "secret": "x"} for i in range(1, 6)]
book_fields = {"id": fields.Integer, "title": fields.String, "price": fields.Float}

list_parser = reqparse.RequestParser()
list_parser.add_argument("page", type=int, default=1, location="args")
list_parser.add_argument("per_page", type=int, default=2, location="args")
list_parser.add_argument("q", location="args")

new_parser = reqparse.RequestParser()
new_parser.add_argument("title", required=True, help="title is required")
new_parser.add_argument("price", type=int, default=0)

class Books(Resource):
    def get(self):
        a = list_parser.parse_args()
        rows = [b for b in ITEMS if not a["q"] or a["q"] in b["title"]]
        start = (a["page"] - 1) * a["per_page"]
        return {"page": a["page"], "total": len(rows),
                "items": [marshal(b, book_fields) for b in rows[start:start + a["per_page"]]]}

    @marshal_with(book_fields, envelope="book")
    def post(self):
        a = new_parser.parse_args()
        b = {"id": len(ITEMS) + 1, "title": a["title"], "price": a["price"]}
        ITEMS.append(b)
        return b, 201

class Book(Resource):
    @marshal_with(book_fields)
    def get(self, bid):
        for b in ITEMS:
            if b["id"] == bid:
                return b
        abort(404, message=f"Book {bid} not found")

api.add_resource(Books, "/api/v1/books")
api.add_resource(Book, "/api/v1/books/<int:bid>")

Results from a test client:

  • GET /api/v1/books gives 200 and {"page": 1, "total": 5, "items": [{"id": 1, "title": "Book 1", "price": 10.0}, {"id": 2, ...}]}. The secret field never appears: marshal dropped it. price is 10.0 because fields.Float is used.
  • GET /api/v1/books?page=3&per_page=2 gives the last page with one item (id 5).
  • GET /api/v1/books?q=Book 4 gives total 1.
  • POST /api/v1/books with {"title": "New", "price": 55} gives 201 and {"book": {"id": 6, "title": "New", "price": 55.0}}; without title it gives 400 and {"message": {"title": "title is required"}}.
  • GET /api/v1/books/99 gives 404 and {"message": "Book 99 not found"}.

The /api/v1/ in the path is versioning: when the design must change in a way that breaks clients, you publish /api/v2/ and let the old one live on.

Your turn: 1. Predict the output
salaries = {1: {"name": "Ravi", "basic": 14500}, 2: {"name": "David", "basic": 9500}}

class Salaries(Resource):
    def do_something(self):
        for s in salaries.keys():
            salaries[s]["hra"] = salaries[s]["basic"] * 0.10
    def get(self):
        self.do_something()
        return salaries

api.add_resource(Salaries, "/api/salaries")

What does GET /api/salaries return?

Need a hint?

do_something changes the original dictionary.

Show the full solution

Each entry keeps its name and basic and gains hra: {"1": {"name": "Ravi", "basic": 14500, "hra": 1450.0}, "2": {"name": "David", "basic": 9500, "hra": 950.0}}. JSON keys are always strings, so the integer keys 1 and 2 print as "1" and "2". do_something is an ordinary helper method, not an HTTP verb.

Your turn: 2. URL matching

Two resources: api.add_resource(Default, "/") (its get(self) returns {"output": "Default computed value : 0"}) and api.add_resource(Power, "/<int:x>/<int:y>") (its get(self, x, y) returns {"output": "Computed x^y = " + str(x**y)}). What do GET /, GET /6/2, GET /x=6/y=2 and GET /6 give?

Need a hint?

Count the pieces, and remember each variable is only the value.

Show the full solution

/ gives the default message. /6/2 gives {"output": "Computed x^y = 36"}. /x=6/y=2 is Not Found: x=6 is not an integer, so <int:x> does not match. /6 is Not Found too: only one piece, two are needed. (I ran this.)

Idea 5: Calling and testing an API

curl options to know:

OptionMeaning
-X POSTchoose the HTTP method (GET is the default)
-H "Content-Type: application/json"add a header
-d '{"name":"Asha"}'request body (implies POST if -X is absent)
-vverbose: shows request and response headers (for debugging)
-iprint the response headers followed by the body
curl http://127.0.0.1:5000/api/v1/books/1
curl -X POST http://127.0.0.1:5000/api/v1/books -H "Content-Type: application/json" -d "{\"title\":\"New\",\"price\":55}"
curl -v http://127.0.0.1:5000/api/v1/books
curl -X DELETE http://127.0.0.1:5000/api/v1/books/1

On Windows cmd, JSON in -d must use double quotes with \" escapes (as in line 2); in Git Bash or Linux you can use single quotes. A real question asked which option shows detailed request/response information for debugging: curl -v (-i just adds the response headers to the output).

From Python with the requests library:

import requests
r = requests.post("http://127.0.0.1:5000/api/v1/books", json={"title": "New", "price": 55})
print(r.status_code)      # 201
print(r.json())           # {'book': {...}}
r = requests.get("http://127.0.0.1:5000/api/v1/books", params={"page": 2})

json= sets the body and the Content-Type header for you. Postman is a point-and-click tool for the same: choose a method, enter the URL, put JSON in the Body tab (raw, JSON), and read status and body.

Your turn: 3. Write the curl

Write a command that updates student 1's grade to B using the Student resource above.

Need a hint?

The verb is PUT, the body is JSON, and the header says so.

Show the full solution
curl -X PUT http://127.0.0.1:5000/student/1 -H "Content-Type: application/json" -d "{\"grade\":\"B\"}"

Expected: 200 and {"message": "Student record updated", "student": {"name": "Arjun", "grade": "B"}}. Without the header, Flask 3 answers 415.

In your project

Expose at least one JSON API for your project's main table (CRUD for a resource), with proper status codes and JSON errors. Keep the web page routes (HTML) separate from the API routes (/api/...). In the viva, you may be asked to call it with curl, so keep two or three working commands ready, and the OpenAPI file next to it.

Idea 6: Documenting with OpenAPI (Swagger)

Documentation says what endpoints exist, their parameters, body and responses. In the exam, "what describes available endpoints, request parameters and response formats?" has the answer API documentation / API reference; and "OpenAPI Specification is a way to describe interfaces for RESTful APIs" (it is not a programming language, performance tester or auth library).

An OpenAPI file is YAML (or JSON) with these top-level keys:

KeyPurpose
openapithe version of the spec (for example 3.0.3)
infotitle, version, description
serversbase URLs
pathseach URL, then each method under it
components/schemasreusable data shapes, referenced with $ref
openapi: 3.0.3
info:
  title: Books API
  version: "1.0"
paths:
  /api/v1/books:
    get:
      summary: List books
      parameters:
        - name: page
          in: query
          schema: {type: integer, default: 1}
      responses:
        "200":
          description: A page of books
          content:
            application/json:
              schema:
                type: array
                items: {$ref: "#/components/schemas/Book"}
    post:
      summary: Create a book
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/NewBook"}
      responses:
        "201": {description: Created}
        "400": {description: Missing or invalid title}
  /api/v1/books/{bid}:
    get:
      summary: Get one book
      parameters:
        - name: bid
          in: path
          required: true
          schema: {type: integer}
      responses:
        "200": {description: The book}
        "404": {description: No such book}
components:
  schemas:
    Book:
      type: object
      properties:
        id: {type: integer}
        title: {type: string}
        price: {type: number}
    NewBook:
      type: object
      required: [title]
      properties:
        title: {type: string}
        price: {type: integer}

Points to notice: path variables are written {bid} (curly braces) here, but <int:bid> in Flask; parameters have in: (query, path, header); response codes are strings; the YAML is indentation-sensitive (use spaces, never tabs). Swagger UI or the Swagger Editor reads this file and gives you a page where you can read and try the API.

Idea 7: Pagination, filtering, versioning and error responses

  • Pagination: GET /books?page=2&per_page=10. The server returns one slice and often the total, so clients do not download everything.
  • Filtering and sorting: GET /books?author=Asha&sort=price. Filters go in the query string, resource identity goes in the path.
  • Versioning: put the version in the path (/api/v1/) or a header, so old clients keep working.
  • Error responses: always JSON with the right status, such as {"message": "Book 99 not found"} (404). Flask-RESTful does this for you when you use its abort.
  • Cacheable GET: GET responses can be cached; POST responses normally not.
Your turn: 4. True or false

(a) PUT is idempotent. (b) A REST server stores each client's state between requests. (c) abort(404, message="...") from flask_restful returns JSON. (d) -d '{...}' alone is enough for a JSON POST in Flask 3.

Need a hint?

Recall the constraints and what Flask 3 does.

Show the full solution

(a) True. (b) False: REST is stateless. (c) True. (d) False: you need -H "Content-Type: application/json"; otherwise 415.

Exam radar

Exam radar

The evidence counts questions whose text matches week 6 topics (keyword match, approximate): about 24 of 692 in Quiz 1, about 82 of 624 in Quiz 2 and about 108 of 1600 in End Term, with 56 distinct questions in our sample. Many Quiz 2 items are "read this Flask-RESTful class and a URL or curl command, then say the output".

What the sample asks:

  • Which curl command or URL works / throws an error, depending on methods defined, add_resource URLs and URL variables (about 6 questions in the sample).
  • Predict the JSON a resource returns (including marshal_with dropping fields and converting types, and the reqparse result {"n": 3}).
  • Status codes and mimetype of a jsonify response (200 and application/json).
  • Match GET, PUT, POST with their URLs; read a PUT that updates a database row and print the final state.
  • Theory: what OpenAPI is, what documentation contains, valid JSON, curl -v, form-encoded versus JSON.
Watch out - real exam trap
  • A URL variable like <int:x> means the URL has just the value, /6/2; x=6/y=2 in the path is Not Found.
  • Only the method decides which Python method runs; the URL text (such as /api/post) means nothing.
  • curl with -d '{...}' but no Content-Type: application/json header is not a JSON request.
  • marshal_with removes keys that are not in the fields dictionary.
  • JSON keys are always strings, so integer dictionary keys print with quotes.
  • A method whose parameters do not match the URL variables is a runtime (500) error, not a 404.

Quick quiz

Quick quiz

Which HTTP method is NOT idempotent?

  • GET
  • PUT
  • DELETE
  • POST
Posting twice normally creates two records; GET, PUT and DELETE can be repeated safely.

A resource class defines only get and put. A client sends POST to its URL. The response is...

  • 404 Not Found
  • 405 Method Not Allowed
  • 200 with an empty body
  • 201 Created
The URL exists, but that verb has no method.

Which status code fits "a new record was created"?

  • 200
  • 201
  • 204
  • 301
201 Created is the standard answer to a successful POST that creates something.

A resource uses @marshal_with({"Id": fields.Integer, "name": fields.String}) and returns {"Id": "3", "name": "u", "email": "e@x.in"}. The JSON is...

  • {"Id": "3", "name": "u", "email": "e@x.in"}
  • {"Id": 3, "name": "u"}
  • {"name": "u", "email": "e@x.in"}
  • an error
The email key is not in the fields, and Integer converts "3" to 3.

Select all statements that are TRUE about REST. (select all that apply)

  • It is stateless.
  • Resources are identified by URLs.
  • The server must remember the last request of every client.
  • It can only return HTML.
REST is stateless and URL based; it commonly returns JSON.

In an OpenAPI file, which top-level section lists the URLs and their methods?

  • info
  • paths
  • components only
  • servers
paths holds each URL and the operations under it; info is title and version.

Viva: say it out loud

Viva: say it out loud
What is a REST API?

It is an API on top of HTTP in which each thing (a resource) has a URL and we use the standard verbs on it: GET to read, POST to create, PUT to update and DELETE to remove. It is stateless, so each request carries what the server needs, and it normally exchanges JSON.

Why is REST called stateless?

The server does not keep client information between calls. Each request has the data (and any token) needed to process it. That makes it easy to scale, because any server can handle any request.

Which status codes does your API return and when?

200 for successful reads and updates, 201 after creating a record, 204 after delete, 400 for bad input, 401 or 403 for authentication and permission problems, 404 when the resource does not exist, 409 for duplicates and 500 only for my own bugs.

Difference between reqparse and marshal_with?

reqparse handles input: it reads, converts and validates the request arguments and returns a 400 if they are wrong. marshal_with handles output: it decides which fields appear in the JSON and converts their types.

How is an API route different from a normal Flask route?

A normal route returns HTML for a browser, using a template. An API route returns JSON with a status code, errors are in JSON, and it is used by a program such as a Vue page or a script. In my project the API routes start with /api/.

How did you document and test your API?

I wrote an OpenAPI YAML file with info, paths, parameters, request bodies, responses and component schemas, and I tested every endpoint with curl and Postman. I can show a POST with a JSON body and the 201 response.

Why POST for create and PUT for update?

POST sends data to a collection and the server creates a new item each time, so repeating it is not safe. PUT sets the state of one known item, so repeating it gives the same result: it is idempotent.

What is versioning and why use it?

It means putting a version such as /api/v1/ in the URL. If I must change the response format later, I add v2 and the old clients still work on v1.

Recap

Remember this
  • An API is a contract between programs; REST is resources plus URLs plus standard verbs, with client-server, stateless, cacheable, uniform interface and layered constraints.
  • CRUD maps to POST, GET, PUT, DELETE. GET is safe and idempotent; PUT and DELETE are idempotent; POST is not.
  • Status codes: 200, 201, 204, 400, 401, 403, 404, 405, 409, 422, 500.
  • Flask-RESTful: Api(app), Resource subclasses with get/post/put/delete, add_resource(Class, url, ...); method arguments must match URL variables.
  • reqparse validates input (set location="args" for query values); marshal_with shapes output; abort(code, message=...) sends a JSON error.
  • JSON needs double quotes; curl needs -H "Content-Type: application/json" for a JSON body; -v shows headers.
  • OpenAPI YAML: openapi, info, paths (parameters, requestBody, responses), components/schemas.
  • Pagination and filtering use the query string; version the API in the path.
Code and facts to keep
  • api = Api(app); api.add_resource(Cls, "/x", "/x/<int:id>")
  • class Cls(Resource): def get(self, id): return {...}, 200
  • parser = reqparse.RequestParser(); parser.add_argument("n", type=int, required=True, location="json"); args = parser.parse_args()
  • @marshal_with({"id": fields.Integer, "name": fields.String})
  • abort(404, message="...") from flask_restful
  • curl -X POST URL -H "Content-Type: application/json" -d "{\"a\":1}"; curl -v URL
  • requests.post(URL, json={...}), .status_code, .json()
  • 201 created, 204 no content, 400 bad input, 401 not logged in, 403 not allowed, 404 missing, 405 wrong verb, 409 conflict
  • OpenAPI: openapi: 3.0.3, info, paths: /x: get: parameters/responses, components: schemas
Connected weeks (learn them together)

Practise with real exam questions

These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week   All Quiz 2 papers