Skip to main content

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:

bash
pip install flask

Understanding REST Principles​

REST is built around a few key principles:

  1. Resources: Everything is a resource identified by a URL
  2. HTTP Methods: Using standard HTTP methods for operations (GET, POST, PUT, DELETE)
  3. Statelessness: Each request contains all necessary information
  4. 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:

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

python
# 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​

python
# 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​

python
# 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​

python
# 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:

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

bash
# 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:

python
@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:

python
@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:

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

  1. Use proper HTTP methods: GET for retrieving data, POST for creating, PUT/PATCH for updating, DELETE for removing.

  2. Return appropriate status codes: 200 for success, 201 for creation, 400 for client errors, 404 for not found, etc.

  3. Keep your API versioned: Use URL prefixes like /api/v1/ to allow for future changes without breaking existing clients.

  4. Use meaningful URLs: Resources should be nouns, not verbs. Use /api/books instead of /api/getBooks.

  5. Implement pagination: For endpoints that return many items, use pagination to limit response size.

  6. Use proper error handling: Return informative error messages with appropriate status codes.

  7. Validate input data: Always validate data received from clients before processing.

  8. 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​

  1. Add authentication to the Todo API using Flask-JWT
  2. Implement pagination for the /api/books and /api/todos endpoints
  3. Connect the APIs to a real database using SQLAlchemy
  4. Add filtering options to the Book API (e.g., filter by author)
  5. Create documentation for your API using Swagger/OpenAPI

Additional Resources​



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)