Flask REST Basics
Introduction
REST (Representational State Transfer) is an architectural style for designing networked applications. RESTful APIs use HTTP requests to perform CRUD operations (Create, Read, Update, Delete) on resources. Flask, being a lightweight and flexible Python web framework, provides an excellent foundation for building RESTful APIs.
In this tutorial, we'll learn how to create a basic REST API using Flask. We'll cover the fundamental concepts of REST, how to implement various HTTP methods, and how to structure your API endpoints effectively.
Prerequisites
Before we begin, make sure you have:
- Basic knowledge of Python
- Understanding of HTTP concepts (requests, responses, methods)
- Flask installed in your environment
If you haven't installed Flask yet, you can do so with pip:
pip install flask
Understanding REST Principles
REST is built around a few key principles:
- Resources: Everything is a resource identified by a URL
- HTTP Methods: Using standard HTTP methods for operations (GET, POST, PUT, DELETE)
- Statelessness: Each request contains all necessary information
- Representation: Resources can have multiple representations (JSON, XML, etc.)
Setting Up a Basic Flask REST API
Let's start by creating a simple Flask application that will serve as our REST API:
from flask import Flask, request, jsonify
app = Flask(__name__)
# Sample data - in a real application, this would be a database
books = [
{"id": 1, "title": "The Hobbit", "author": "J.R.R. Tolkien"},
{"id": 2, "title": "Harry Potter", "author": "J.K. Rowling"},
{"id": 3, "title": "Clean Code", "author": "Robert C. Martin"}
]
@app.route('/')
def home():
return "Welcome to the Book API!"
if __name__ == '__main__':
app.run(debug=True)
Implementing HTTP Methods
Now let's implement the four main HTTP methods that correspond to CRUD operations:
GET - Reading Resources
# Get all books
@app.route('/api/books', methods=['GET'])
def get_books():
return jsonify({"books": books})
# Get a specific book by ID
@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
book = next((book for book in books if book["id"] == book_id), None)
if book:
return jsonify(book)
return jsonify({"error": "Book not found"}), 404
POST - Creating Resources
# Create a new book
@app.route('/api/books', methods=['POST'])
def add_book():
if not request.json or 'title' not in request.json or 'author' not in request.json:
return jsonify({"error": "Bad request"}), 400
# Generate a new ID (in a real app, the database would handle this)
new_id = max(book["id"] for book in books) + 1
book = {
"id": new_id,
"title": request.json["title"],
"author": request.json["author"]
}
books.append(book)
return jsonify(book), 201
PUT - Updating Resources
# Update an existing book
@app.route('/api/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
book = next((book for book in books if book["id"] == book_id), None)
if not book:
return jsonify({"error": "Book not found"}), 404
if not request.json:
return jsonify({"error": "Bad request"}), 400
book["title"] = request.json.get("title", book["title"])
book["author"] = request.json.get("author", book["author"])
return jsonify(book)
DELETE - Removing Resources
# Delete a book
@app.route('/api/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
global books
book = next((book for book in books if book["id"] == book_id), None)
if not book:
return jsonify({"error": "Book not found"}), 404
books = [book for book in books if book["id"] != book_id]
return jsonify({"result": "Book deleted"})
Complete Example
Let's put everything together to create our complete REST API:
from flask import Flask, request, jsonify
app = Flask(__name__)
# Sample data - in a real application, this would be a database
books = [
{"id": 1, "title": "The Hobbit", "author": "J.R.R. Tolkien"},
{"id": 2, "title": "Harry Potter", "author": "J.K. Rowling"},
{"id": 3, "title": "Clean Code", "author": "Robert C. Martin"}
]
@app.route('/')
def home():
return "Welcome to the Book API!"
# Get all books
@app.route('/api/books', methods=['GET'])
def get_books():
return jsonify({"books": books})
# Get a specific book by ID
@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
book = next((book for book in books if book["id"] == book_id), None)
if book:
return jsonify(book)
return jsonify({"error": "Book not found"}), 404
# Create a new book
@app.route('/api/books', methods=['POST'])
def add_book():
if not request.json or 'title' not in request.json or 'author' not in request.json:
return jsonify({"error": "Bad request"}), 400
# Generate a new ID (in a real app, the database would handle this)
new_id = max(book["id"] for book in books) + 1
book = {
"id": new_id,
"title": request.json["title"],
"author": request.json["author"]
}
books.append(book)
return jsonify(book), 201
# Update an existing book
@app.route('/api/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
book = next((book for book in books if book["id"] == book_id), None)
if not book:
return jsonify({"error": "Book not found"}), 404
if not request.json:
return jsonify({"error": "Bad request"}), 400
book["title"] = request.json.get("title", book["title"])
book["author"] = request.json.get("author", book["author"])
return jsonify(book)
# Delete a book
@app.route('/api/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
global books
book = next((book for book in books if book["id"] == book_id), None)
if not book:
return jsonify({"error": "Book not found"}), 404
books = [book for book in books if book["id"] != book_id]
return jsonify({"result": "Book deleted"})
if __name__ == '__main__':
app.run(debug=True)
Testing Your API
You can test your API using various tools:
Using curl
# Get all books
curl http://localhost:5000/api/books
# Get a specific book
curl http://localhost:5000/api/books/1
# Create a new book
curl -X POST -H "Content-Type: application/json" -d '{"title":"1984","author":"George Orwell"}' http://localhost:5000/api/books
# Update a book
curl -X PUT -H "Content-Type: application/json" -d '{"title":"The Lord of the Rings"}' http://localhost:5000/api/books/1
# Delete a book
curl -X DELETE http://localhost:5000/api/books/2
Using a REST client
Tools like Postman or Insomnia provide a user-friendly interface for testing APIs.
Status Codes and Error Handling
Proper status codes are crucial for a REST API. Here are some common status codes:
- 200 OK: The request was successful
- 201 Created: A new resource was successfully created
- 400 Bad Request: The request was malformed
- 404 Not Found: The requested resource was not found
- 500 Internal Server Error: Server error
Let's improve our error handling:
@app.errorhandler(400)
def bad_request(error):
return jsonify({"error": "Bad request"}), 400
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Resource not found"}), 404
@app.errorhandler(500)
def server_error(error):
return jsonify({"error": "Server error"}), 500
Content Negotiation
A well-designed REST API should support different formats. Flask makes this easy:
@app.route('/api/books/<int:book_id>')
def get_book(book_id):
book = next((book for book in books if book["id"] == book_id), None)
if not book:
return jsonify({"error": "Book not found"}), 404
# Check if client wants JSON or HTML
if request.headers.get('Accept') == 'text/html':
return f"""
<html>
<body>
<h1>{book["title"]}</h1>
<p>By {book["author"]}</p>
</body>
</html>
"""
else:
return jsonify(book)
Real-world Application: Building a Todo API
Let's create a more practical example - a Todo API:
from flask import Flask, request, jsonify
from datetime import datetime
app = Flask(__name__)
todos = [
{
"id": 1,
"title": "Learn Flask",
"description": "Study Flask REST APIs",
"done": False,
"created_at": "2023-01-15T10:30:00Z"
},
{
"id": 2,
"title": "Build project",
"description": "Create a REST API with Flask",
"done": False,
"created_at": "2023-01-16T14:20:00Z"
}
]
# Get all todos or filter by status
@app.route('/api/todos', methods=['GET'])
def get_todos():
status = request.args.get('status')
if status:
if status.lower() == 'done':
filtered_todos = [todo for todo in todos if todo['done']]
elif status.lower() == 'pending':
filtered_todos = [todo for todo in todos if not todo['done']]
else:
return jsonify({"error": "Invalid status parameter"}), 400
return jsonify({"todos": filtered_todos})
return jsonify({"todos": todos})
# Get a specific todo
@app.route('/api/todos/<int:todo_id>', methods=['GET'])
def get_todo(todo_id):
todo = next((todo for todo in todos if todo["id"] == todo_id), None)
if todo:
return jsonify(todo)
return jsonify({"error": "Todo not found"}), 404
# Create a new todo
@app.route('/api/todos', methods=['POST'])
def add_todo():
if not request.json or 'title' not in request.json:
return jsonify({"error": "Title is required"}), 400
new_id = max(todo["id"] for todo in todos) + 1
todo = {
"id": new_id,
"title": request.json["title"],
"description": request.json.get("description", ""),
"done": False,
"created_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
}
todos.append(todo)
return jsonify(todo), 201
# Update a todo
@app.route('/api/todos/<int:todo_id>', methods=['PUT'])
def update_todo(todo_id):
todo = next((todo for todo in todos if todo["id"] == todo_id), None)
if not todo:
return jsonify({"error": "Todo not found"}), 404
if not request.json:
return jsonify({"error": "Bad request"}), 400
todo["title"] = request.json.get("title", todo["title"])
todo["description"] = request.json.get("description", todo["description"])
todo["done"] = request.json.get("done", todo["done"])
return jsonify(todo)
# Delete a todo
@app.route('/api/todos/<int:todo_id>', methods=['DELETE'])
def delete_todo(todo_id):
global todos
todo = next((todo for todo in todos if todo["id"] == todo_id), None)
if not todo:
return jsonify({"error": "Todo not found"}), 404
todos = [todo for todo in todos if todo["id"] != todo_id]
return jsonify({"result": "Todo deleted"})
# Mark a todo as done
@app.route('/api/todos/<int:todo_id>/toggle', methods=['PATCH'])
def toggle_todo(todo_id):
todo = next((todo for todo in todos if todo["id"] == todo_id), None)
if not todo:
return jsonify({"error": "Todo not found"}), 404
todo["done"] = not todo["done"]
return jsonify(todo)
if __name__ == '__main__':
app.run(debug=True)
Best Practices for Flask REST APIs
-
Use proper HTTP methods: GET for retrieving data, POST for creating, PUT/PATCH for updating, DELETE for removing.
-
Return appropriate status codes: 200 for success, 201 for creation, 400 for client errors, 404 for not found, etc.
-
Keep your API versioned: Use URL prefixes like
/api/v1/
to allow for future changes without breaking existing clients. -
Use meaningful URLs: Resources should be nouns, not verbs. Use
/api/books
instead of/api/getBooks
. -
Implement pagination: For endpoints that return many items, use pagination to limit response size.
-
Use proper error handling: Return informative error messages with appropriate status codes.
-
Validate input data: Always validate data received from clients before processing.
-
Implement authentication and authorization: Protect your API endpoints from unauthorized access.
Summary
In this tutorial, we've learned:
- The basics of REST architecture
- How to create a simple Flask application to serve as a REST API
- Implementing CRUD operations using HTTP methods (GET, POST, PUT, DELETE)
- Adding proper error handling and status codes
- Building a practical Todo API as a real-world example
- Best practices for designing RESTful APIs
REST APIs are a powerful way to expose your application's functionality to other services and clients. Flask provides a simple yet flexible framework for building these APIs with Python.
Further Exercises
- Add authentication to the Todo API using Flask-JWT
- Implement pagination for the
/api/books
and/api/todos
endpoints - Connect the APIs to a real database using SQLAlchemy
- Add filtering options to the Book API (e.g., filter by author)
- Create documentation for your API using Swagger/OpenAPI
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)