Understanding RESTful APIs with Flask-RESTful
RESTful APIs are essential for modern web development, allowing seamless communication between a client and a server. In Flask, we can create RESTful APIs efficiently using the Flask-RESTful extension.
1. What is an API?
An API (Application Programming Interface) is a set of rules that allows software applications to communicate with each other. APIs enable data exchange between systems and provide a standardized way for applications to interact.
2. Why Use an API?
- Scalability: APIs enable applications to grow and integrate with multiple platforms.
- Efficiency: Reduces redundancy by allowing different systems to share functionality.
- Automation: Automates tasks and workflows between services.
- Security: Ensures controlled access to data using authentication mechanisms.
3. When to Use an API?
- When different applications need to communicate.
- To expose data or services to external applications.
- When developing microservices architecture.
4. How APIs Work
A client sends a request to the server, and the server processes it and returns a response. APIs follow a request-response cycle:
- A client sends a request to an API.
- The API processes the request and interacts with the database if needed.
- The API returns a response to the client.
π‘ Too Much used Analogy: Imagine ordering food at a restaurant. The waiter (API) takes your order (request), gives it to the kitchen (server), and brings back the food (response).
5. Hierarchy: From Route to Flask-RESTful API
Understanding the progression from a simple route to a full-fledged Flask-RESTful API is crucial for building scalable applications. Below is a structured hierarchy:
-
Flask Route (
@app.route): The most basic way to create an API using Flask. It defines individual endpoints manually and maps them to functions.- Example:
@app.route('/hello', methods=['GET']) def hello(): return "Hello, World!"
- Example:
-
API (Application Programming Interface): A broader concept that refers to any defined mechanism allowing applications to interact. APIs serve data through structured endpoints.
-
RESTful API: A RESTful API follows the principles of REST (Representational State Transfer) to enable efficient client-server communication. It follows standard HTTP methods:
| HTTP Method | Operation | CRUD |
|---|---|---|
| GET | Retrieve data | Read |
| POST | Create new data | Create |
| PUT | Update existing data | Update |
| DELETE | Remove data | Delete |
-
Flask-RESTful API: A structured and scalable way to build RESTful APIs using the Flask-RESTful extension. It organizes endpoints using class-based resources, making it easier to manage complex APIs.
- Example:
from flask_restful import Resource, Api class Hello(Resource): #remember class from flask_sqlalchemy it aslo takes db.Model similar to Resource here def get(self): return {"message": "Hello, World!"}
- Example:
This hierarchy shows how API development progresses from a simple function-based approach to a more scalable, structured Flask-RESTful approach.
6. Difference Between Flask Route and Flask-RESTful
| Feature | Flask Route | Flask-RESTful |
|---|---|---|
| API Creation | Uses @app.route decorator |
Uses Resource class and add_resource method |
| HTTP Methods | Needs explicit function definitions | Handles multiple HTTP methods using class methods |
| Code Organization | Less structured, function-based | More structured, class-based |
| Scalability | Can become complex with many routes | Easier to manage as API grows |
| Data Parsing | Uses request.json for input |
Uses reqparse.RequestParser for structured input handling |
| Response Format | Uses jsonify() for response |
Returns a dictionary, automatically converted to JSON |
Code Comparison: Jinja2 vs Flask-RESTful
Using Jinja2 (render_template)
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
Using Flask-RESTful
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
api = Api(app)
class Home(Resource):
def get(self):
return {"message": "Welcome to the API"}
api.add_resource(Home, '/')
if __name__ == '__main__':
app.run(debug=True)
7. HTTP Methods and Implementation
HTTP methods allow interaction with APIs. Below is a comparison of Flask route and Flask-RESTful for different HTTP methods.
Flask Route Example
from flask import Flask, request, jsonify
app = Flask(__name__)
users = {}
@app.route('/user/<string:user_id>', methods=['GET'])
def get_user(user_id):
return jsonify(users.get(user_id, "User not found"))
@app.route('/user/<string:user_id>', methods=['POST'])
def create_user(user_id):
users[user_id] = request.json
return jsonify({"message": "User added", "data": users[user_id]})
@app.route('/user/<string:user_id>', methods=['PUT'])
def update_user(user_id):
users[user_id] = request.json
return jsonify({"message": "User updated", "data": users[user_id]})
@app.route('/user/<string:user_id>', methods=['DELETE'])
def delete_user(user_id):
users.pop(user_id, None)
return jsonify({"message": "User deleted"})
if __name__ == '__main__':
app.run(debug=True)
HTML Form Restriction Example
<form action="/user/1" method="post">
<input type="text" name="name" placeholder="Enter Name">
<input type="submit" value="Submit">
</form>
Note: HTML forms do not support PUT or DELETE methods.
Flask-RESTful with Database Example
from flask import Flask, request, jsonify
from flask_restful import Resource, Api, reqparse
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(app)
api = Api(app)
class UserModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
email = db.Column(db.String(100))
with app.app_context():
db.create_all()
class User(Resource):
def get(self, user_id):
user = UserModel.query.get(user_id)
if user:
return jsonify({"id": user.id, "name": user.name, "email": user.email})
return jsonify({"message": "User not found"})
def post(self, user_id):
data = request.get_json()
user = UserModel(id=user_id, name=data['name'], email=data['email'])
db.session.add(user)
db.session.commit()
return jsonify({"message": "User added", "data": data})
def put(self, user_id):
user = UserModel.query.get(user_id)
if not user:
return jsonify({"message": "User not found"})
data = request.get_json()
user.name = data.get('name', user.name)
user.email = data.get('email', user.email)
db.session.commit()
return jsonify({"message": "User updated", "data": {"name": user.name, "email": user.email}})
def delete(self, user_id):
user = UserModel.query.get(user_id)
if not user:
return jsonify({"message": "User not found"})
db.session.delete(user)
db.session.commit()
return jsonify({"message": "User deleted"})
api.add_resource(User, '/user/<int:user_id>')
if __name__ == '__main__':
app.run(debug=True)
8. Fetch API (Client-Side Usage)
fetch('http://127.0.0.1:5000/user/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Example of a Simple REST API
To illustrate the REST concept, letβs create a simple API using Flaskβs route decorator. This API will allow us to retrieve and create users.
Install Flask-RESTful
pip install flask flask-restful flask-sqlalchemy
Using Flask Route with jsonify:
from flask import Flask, jsonify, request
app = Flask(__name__)
users = {"1": {"name": "Alice", "email": "alice@example.com"}}
@app.route('/user/<string:user_id>', methods=['GET'])
def get_user(user_id):
return jsonify(users.get(user_id, "User not found"))
@app.route('/user/<string:user_id>', methods=['POST'])
def create_user(user_id):
users[user_id] = request.json
return jsonify({"message": "User added", "data": users[user_id]})
if __name__ == '__main__':
app.run(debug=True)
Another Way to Create APIs: Using Flask with Response Object
Instead of using jsonify, we can directly use the Response object to create API responses:
from flask import Flask, Response, request
import json
app = Flask(__name__)
users = {"1": {"name": "Alice", "email": "alice@example.com"}}
@app.route('/user/<string:user_id>', methods=['GET'])
def get_user(user_id):
response = json.dumps(users.get(user_id, {"error": "User not found"}))
return Response(response, status=200, mimetype='application/json')
@app.route('/user/<string:user_id>', methods=['POST'])
def create_user(user_id):
users[user_id] = request.json
response = json.dumps({"message": "User added", "data": users[user_id]})
return Response(response, status=201, mimetype='application/json')
if __name__ == '__main__':
app.run(debug=True)
9. Conclusion
Flask-RESTful makes it easy to build structured, scalable REST APIs with Flask. It improves API management by enforcing REST principles and simplifying HTTP method handling. Using JSON for data exchange and YAML for documentation ensures standardization and ease of use.
Extra
Breakdown of the code defines a Flask-RESTful API with SQLAlchemy for managing users.
1. Imports and Flask App Initialization
from flask import Flask, request, jsonify
from flask_restful import Resource, Api, reqparse
from flask_sqlalchemy import SQLAlchemy
Flask: The core Flask framework for building the web application.request: Used to handle incoming request data (e.g., JSON).jsonify: Converts Python dictionaries into JSON responses.flask_restful.Resource: A class-based approach to defining API endpoints.flask_restful.Api: Wraps the Flask app to support RESTful routes.reqparse: Helps validate and parse request arguments.flask_sqlalchemy.SQLAlchemy: SQLAlchemy ORM for database operations.
2. Flask App and Database Configuration
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(app)
api = Api(app)
with app.app_context():
db.create_all()
- Creates a Flask app instance.
- Configures the app to use an SQLite database (
users.db). - Initializes SQLAlchemy (
db) for ORM functionality. - Initializes Flask-RESTful API (
api). - Creates a context with the app for database operations. and creates the database tables.
3. Defining the UserModel (Database Table)
class UserModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
email = db.Column(db.String(100))
UserModel: Represents auserstable in the database.id: An integer column that acts as the primary key.name: A string column for storing the userβs name.email: A string column for storing the userβs email.
4. Creating the User Resource
class User(Resource):
def get(self, user_id):
user = UserModel.query.get(user_id)
if user:
return jsonify({"id": user.id, "name": user.name, "email": user.email})
return jsonify({"message": "User not found"})
get(self, user_id): Fetches a user by ID.UserModel.query.get(user_id): Retrieves the user from the database.- If the user exists, returns their details as JSON.
- If not found, returns a JSON message:
{"message": "User not found"}.
5. Handling User Creation
def post(self, user_id):
data = request.get_json()
user = UserModel(id=user_id, name=data['name'], email=data['email'])
db.session.add(user)
db.session.commit()
return jsonify({"message": "User added", "data": data})
post(self, user_id): HandlesPOSTrequests to add a new user.request.get_json(): Extracts JSON data from the request body.- Creates a new
UserModelobject and adds it to the database. - Commits changes using
db.session.commit(). - Returns a success message.
6. Updating an Existing User
def put(self, user_id):
user = UserModel.query.get(user_id)
if not user:
return jsonify({"message": "User not found"})
data = request.get_json()
user.name = data.get('name', user.name)
user.email = data.get('email', user.email)
db.session.commit()
return jsonify({"message": "User updated", "data": {"name": user.name, "email": user.email}})
put(self, user_id): Updates an existing user.- Fetches the user by
user_id. If not found, returns a message. - Uses
data.get('key', default_value)to update only provided fields. - Saves changes using
db.session.commit(). - Returns the updated user details.
7. Deleting a User
def delete(self, user_id):
user = UserModel.query.get(user_id)
if not user:
return jsonify({"message": "User not found"})
db.session.delete(user)
db.session.commit()
return jsonify({"message": "User deleted"})
delete(self, user_id): Deletes a user.- Fetches the user by
user_id. If not found, returns a message. - Deletes the user from the database using
db.session.delete(user). - Commits the deletion using
db.session.commit(). - Returns a success message.
8. Adding Resource to the API
api.add_resource(User, '/user/<int:user_id>')
- Maps the
Userclass to the/user/<user_id>route. <int:user_id>ensuresuser_idis an integer.
9. Running the Application
if __name__ == '__main__':
app.run(debug=True)
app.run(debug=True): Starts the Flask server in debug mode.
πΉ Summary of CRUD Operations
| HTTP Method | Endpoint | Description |
|---|---|---|
GET |
/user/1 |
Fetches user with ID 1. |
POST |
/user/1 + JSON body |
Creates a user with ID 1. |
PUT |
/user/1 + JSON body |
Updates user with ID 1. |
DELETE |
/user/1 |
Deletes user with ID 1. |
πΉ Testing with Thunder Client or Postman
-
GET Request
- URL:
http://127.0.0.1:5000/user/1 - Response:
{ "id": 1, "name": "John Doe", "email": "john@example.com" }
- URL:
-
POST Request
- URL:
http://127.0.0.1:5000/user/1 - Headers:
{ "Content-Type": "application/json" } - Body:
{ "name": "John Doe", "email": "john@example.com" } - Response:
{ "message": "User added", "data": { "name": "John Doe", "email": "john@example.com" } }
- URL:
-
PUT Request
- URL:
http://127.0.0.1:5000/user/1 - Headers:
{ "Content-Type": "application/json" } - Body:
{ "name": "Jane Doe", "email": "jane@example.com" } - Response:
{ "message": "User updated", "data": { "name": "Jane Doe", "email": "jane@example.com" } }
- URL:
-
DELETE Request
- URL:
http://127.0.0.1:5000/user/1 - Response:
{ "message": "User deleted" }
- URL:
πΉ Key Takeaways
β
Flask-RESTful organizes APIs using class-based resources.
β
SQLAlchemy provides ORM support for interacting with the database.
β
GET, POST, PUT, DELETE enable full CRUD functionality.
β
Thunder Client/Postman can be used to test API requests.
β
db.create_all() ensures the database is created before running.
Idempotency in HTTP Verbs
Idempotency means that making multiple identical requests has the same effect as making a single request. In other words, the outcome remains unchanged no matter how many times the request is sent.
Idempotent HTTP Methods
These methods ensure that repeated requests do not change the server state after the first successful request.
-
GET:
- Retrieves data from the server.
- Multiple GET requests return the same result without modifying data.
- Example: Fetching user details (
GET /user/1) multiple times does not alter the user.
-
PUT:
- Updates or creates a resource at a specified location.
- Sending the same
PUTrequest repeatedly results in the same final state. - Example:
PUT /user/1with{ "name": "John" }will always update user 1 to "John" regardless of how many times it is sent.
-
DELETE:
- Removes a resource.
- Multiple DELETE requests for the same resource will always result in the resource being removed or remain absent.
- Example:
DELETE /user/1ensures user 1 is deleted, and subsequent calls do nothing.
-
HEAD:
- Similar to
GET, but it only returns headers without the response body. - It does not modify any resources, so it is idempotent.
- Similar to
-
OPTIONS:
- Used to describe the communication options available for a resource.
- Making multiple requests does not change anything.
Non-Idempotent HTTP Methods
These methods do not guarantee the same outcome when called multiple times.
-
POST:
- Creates a new resource.
- Sending multiple
POSTrequests results in multiple resources being created. - Example:
POST /userwith{ "name": "Alice" }creates multiple user entries.
-
PATCH:
- Partially updates a resource.
- Multiple
PATCHrequests may lead to different states depending on how they are applied. - Example: Incrementing a counter in a
PATCHrequest changes the state each time.
Key Takeaways
- Idempotent methods:
GET,PUT,DELETE,HEAD,OPTIONS - Non-idempotent methods:
POST,PATCH - Idempotency helps prevent unintended side effects when requests are retried due to network failures.
Here's a Flask example demonstrating idempotency for PUT, DELETE, and POST using SQLite and SQLAlchemy.
Scenario:
- We have an API for managing users (
id,name). PUT /users/<id>ensures idempotent updates.DELETE /users/<id>ensures idempotent deletion.POST /usersdemonstrates non-idempotent behavior.
Flask Application with Idempotency Handling
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///users.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
# User model
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
# Initialize database
with app.app_context():
db.create_all()
# β
IDEMPOTENT: PUT (Update or Create)
@app.route("/users/<int:user_id>", methods=["PUT"])
def update_user(user_id):
data = request.get_json()
user = User.query.get(user_id)
if user: # If user exists, update it
user.name = data["name"]
else: # If user doesn't exist, create it (still idempotent)
user = User(id=user_id, name=data["name"])
db.session.add(user)
db.session.commit()
return jsonify({"id": user.id, "name": user.name}), 200
# β
IDEMPOTENT: DELETE (Always results in resource absence)
@app.route("/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id):
user = User.query.get(user_id)
if user: # Delete if exists
db.session.delete(user)
db.session.commit()
return jsonify({"message": "User deleted"}), 200
else: # If already deleted, response remains same
return jsonify({"message": "User not found"}), 200
# β NON-IDEMPOTENT: POST (Creates new entry every time)
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
new_user = User(name=data["name"])
db.session.add(new_user)
db.session.commit()
return jsonify({"id": new_user.id, "name": new_user.name}), 201
# Run the app
if __name__ == "__main__":
app.run(debug=True)
Testing the API
1. PUT Request (Idempotent)
curl -X PUT http://127.0.0.1:5000/users/1 -H "Content-Type: application/json" -d '{"name": "Alice"}'
- Running it multiple times does not create duplicate users, only updates user 1.
2. DELETE Request (Idempotent)
curl -X DELETE http://127.0.0.1:5000/users/1
- Running it multiple times ensures user 1 is deleted or already absent.
3. POST Request (Non-Idempotent)
curl -X POST http://127.0.0.1:5000/users -H "Content-Type: application/json" -d '{"name": "Bob"}'
- Running it multiple times creates new users (e.g., Bob with different IDs).
Key Takeaways
β
PUT is idempotent β No matter how many times you update user 1, the result remains the same.
β
DELETE is idempotent β User 1 remains deleted regardless of retries.
β POST is not idempotent β Creates a new user every time.
JSON vs YAML
| Feature | JSON (JavaScript Object Notation) | YAML (YAML Ain't Markup Language) |
|---|---|---|
| Format | Text-based, structured like JavaScript objects | Human-readable, uses indentation |
| Data Types | Supports numbers, strings, lists, dictionaries (objects) | Supports all JSON types + complex types (e.g., multiline strings) |
| Syntax | Uses {} for objects and [] for lists |
Uses indentation instead of brackets |
| Readability | Machine-friendly, compact | Human-friendly, clean format |
| Usage | APIs, web services, databases | Config files (Docker, Kubernetes, Ansible) |
| Example | {"name": "Alice", "age": 30} |
name: Alice\nage: 30 |
π JSON is used for data exchange (APIs, web apps), whereas YAML is used for configuration files.
json.dumps()
json.dumps() converts a Python dictionary into a JSON-formatted string.
import json
data = {"name": "Alice", "age": 30}
json_string = json.dumps(data)
print(json_string) # Output: {"name": "Alice", "age": 30}
π Use case: Serializing Python objects for storage, logging, or API responses.
flask.Response in Flask
Response is used to return HTTP responses with custom headers, content, and status codes.
from flask import Flask, Response, jsonify
app = Flask(__name__)
@app.route("/")
def custom_response():
data = {"message": "Hello, Flask!"}
response = Response(json.dumps(data), status=200, mimetype="application/json")
return response
if __name__ == "__main__":
app.run(debug=True)
π Use case: When you need to manually control the HTTP response.
flask_restful.Resource
In Flask-RESTful, Resource is a class-based way to define REST API endpoints.
from flask import Flask
from flask_restful import Api, Resource
app = Flask(__name__)
api = Api(app)
class HelloWorld(Resource):
def get(self):
return {"message": "Hello, Flask-RESTful!"}
api.add_resource(HelloWorld, "/")
if __name__ == "__main__":
app.run(debug=True)
π Use case: Resource makes REST API development structured and reusable.
Final Thoughts
- β
Use
json.dumps()when converting Python objects to JSON strings. - β
Use
Responsewhen customizing Flask API responses. - β
Use
Resource(Flask-RESTful) to create structured REST APIs. - β Use JSON for APIs, while YAML is better for configurations.
json.dump() vs json.dumps()
Both functions convert Python objects to JSON format, but they differ in their output.
| Function | Purpose | Output |
|---|---|---|
json.dumps() |
Converts Python object to JSON string | Returns a string |
json.dump() |
Writes JSON data to a file | Saves directly to a file |
Example: json.dump()
Writes JSON data to a file.
import json
data = {"name": "Alice", "age": 30}
with open("data.json", "w") as file:
json.dump(data, file) # Saves JSON directly to data.json
π Use case: Storing JSON data in files.
flask.jsonify()
jsonify() is a Flask helper function that automatically converts Python dictionaries into JSON responses and sets the correct Content-Type (application/json).
Example: jsonify()
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def json_response():
data = {"message": "Hello, Flask!"}
return jsonify(data) # Converts to JSON and sets Content-Type
if __name__ == "__main__":
app.run(debug=True)
πΉ Advantages over json.dumps() in Flask:
- Automatically sets
Content-Type: application/json - Handles Unicode characters correctly
- Handles lists and tuples properly
JSON.stringify() (JavaScript)
JSON.stringify() is a JavaScript function that converts a JavaScript object into a JSON string.
Example: JSON.stringify()
const data = { name: "Alice", age: 30 };
const jsonString = JSON.stringify(data);
console.log(jsonString); // Output: {"name":"Alice","age":30}
π Use case: Sending JSON data in AJAX requests or APIs.
Summary
| Function | Language | Purpose |
|---|---|---|
json.dumps() |
Python | Converts Python object to JSON string |
json.dump() |
Python | Writes JSON data to a file |
jsonify() |
Flask (Python) | Converts dictionary to JSON and sets Content-Type |
JSON.stringify() |
JavaScript | Converts JavaScript object to JSON string |
Sure! In Flask-RESTful, marshal and marshal_with are used for formatting API responses according to a specified structure. They help in ensuring that API responses follow a consistent format.
marshal and marshal_with in Flask-RESTful
marshal
- This is a function that manually formats data according to a given structure.
- You must call it explicitly before returning a response.
Example:
from flask_restful import marshal, fields
# Define response structure
resource_fields = {
'id': fields.Integer,
'name': fields.String,
'age': fields.Integer
}
# Sample data
data = {'id': 1, 'name': 'John', 'age': 30, 'extra_field': 'Not needed'}
# Use marshal to format data
formatted_data = marshal(data, resource_fields)
print(formatted_data) # Output: {'id': 1, 'name': 'John', 'age': 30}
Extra fields (extra_field) not mentioned in resource_fields are ignored.
πΉ marshal_with
- This is a decorator used inside resource methods like
get(),post(), etc. - It automatically applies formatting to the return value of the function.
Example:
from flask import Flask
from flask_restful import Api, Resource, fields, marshal_with
app = Flask(__name__)
api = Api(app)
# Define response structure
resource_fields = {
'id': fields.Integer,
'name': fields.String,
'age': fields.Integer
}
class User(Resource):
@marshal_with(resource_fields) # Apply formatting automatically
def get(self):
user_data = {'id': 1, 'name': 'John', 'age': 30, 'extra_field': 'Ignored'}
return user_data # No need to call marshal manually
api.add_resource(User, '/user')
if __name__ == '__main__':
app.run(debug=True)
The response will automatically be:
{
"id": 1,
"name": "John",
"age": 30
}
Difference Between marshal and marshal_with
| Feature | marshal |
marshal_with |
|---|---|---|
| Type | Function | Decorator |
| Usage | Called manually | Used above a method |
| When to Use | If you need to format multiple responses | If formatting is always required for a function's return value |