REST APIs with Flask

INFO 153B/253B: Backend Web Architecture

Week 3

 

Kay Ashaolu - Instructor

Suk Min Hwang - GSI

Today's Agenda

  • Part 1: Prep Work Recap - Flask basics check-in
  • Part 2: From Python to Flask - The decorator is the only difference
  • Part 3: HTTP Methods & REST - Designing RESTful APIs
  • Part 4: Request & Response - Handling data in Flask
  • Part 5: Error Handling - Building robust APIs
  • Demo: From Week 2 Function to Flask API (10 min)
  • In-Class Exploration: Student Records Flask API (45 min)

Part 1: Prep Work Recap

Quick check-in on O'Reilly Chapter 3

What You Learned in Prep

  • Flask setup: pip install flask, create app.py
  • App instance: app = Flask(__name__)
  • First endpoint: @app.get('/store')
  • Running the app: flask run
  • Testing: Insomnia, Postman, or curl
Key insight: Flask takes Python functions and makes them accessible over HTTP.

Quick Check: Flask App Structure

from flask import Flask

app = Flask(__name__)

@app.get('/store')
def get_stores():
    return {"stores": []}

if __name__ == '__main__':
    app.run(debug=True)
  • Flask(__name__) creates the application instance
  • @app.get is a decorator - routes URL to function
  • Return a dictionary - Flask converts to JSON automatically
  • debug=True enables auto-reload and better error messages

Quick Check: JSON from Flask

@app.get('/store')
def get_stores():
    stores = [
        {"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}
    ]
    return {"stores": stores}
  • Flask automatically converts Python dicts/lists to JSON
  • Sets Content-Type: application/json header
  • True becomes true, None becomes null
  • No need to call json.dumps() manually

Quick Check: Testing with curl

# GET request
curl http://localhost:5000/store

# POST request with JSON data
curl -X POST http://localhost:5000/store \
  -H "Content-Type: application/json" \
  -d '{"name": "New Store"}'
  • curl is a command-line HTTP client
  • -X POST specifies the HTTP method
  • -H adds headers (Content-Type tells Flask it's JSON)
  • -d sends data in the request body

Prep Recap: Key Takeaways

  • Flask is minimal: Just the essentials for web apps
  • Decorators route URLs: @app.get('/path')
  • Functions return data: Dicts become JSON responses
  • Testing is essential: curl, Insomnia, or Postman
The connection: Remember Week 2's functions that returned (data, status_code)? Flask routes ARE those functions - just with a decorator on top.

Part 2: From Python to Flask

The decorator is the only difference

Remember Week 2?

# Week 2: Pure Python function
def get_student(student_id):
    """Get a student by ID."""
    student = students.get(student_id)
    if student is None:
        return {"error": "Student not found"}, 404
    return student, 200

# Testing it
result, status = get_student(1)
print(f"Status: {status}")
print(result)
  • You wrote CRUD functions that return (data, status_code)
  • You handled errors with 404, validated with 400
  • You used dictionaries that serialize to JSON
  • You already wrote API logic!

The Transformation

Week 2: Pure Python

def get_student(student_id):
    student = students.get(student_id)
    if student is None:
        return {"error": "Not found"}, 404
    return student, 200

Week 3: Flask API

@app.get('/students/<int:student_id>')
def get_student(student_id):
    student = students.get(student_id)
    if student is None:
        return {"error": "Not found"}, 404
    return student, 200
Look at this! The function body is IDENTICAL. Flask just adds the routing.

What the Decorator Does

@app.get('/students/<int:student_id>')
def get_student(student_id):
    ...
  • @app.get - Listen for GET requests
  • /students/ - At this URL path
  • <int:student_id> - Capture an integer from the URL
  • The captured value becomes a function parameter
URL: GET /students/42
Flask calls: get_student(student_id=42)

All CRUD Operations: The Same Pattern

@app.get('/students')
def list_students():
    return {"students": list(students.values()), "total": len(students)}

@app.get('/students/<int:student_id>')
def get_student(student_id):
    # Same logic as Week 2

@app.post('/students')
def create_student():
    # Same logic as Week 2's add_student

@app.put('/students/<int:student_id>')
def update_student(student_id):
    # Same logic as Week 2

@app.delete('/students/<int:student_id>')
def delete_student(student_id):
    # Same logic as Week 2

Why This Matters

  • You already know the hard part: Business logic, validation, error handling
  • Flask is just glue: Connects HTTP to your Python functions
  • Separation of concerns: Your logic stays pure Python
  • Testability: You can test functions without HTTP
Industry insight: This is why Flask is popular - it's minimal. You write Python. Flask handles HTTP. Pinterest, Stripe, and Netflix use this pattern.

Part 3: HTTP Methods & REST

Designing RESTful APIs

HTTP Methods Map to Actions

MethodActionExampleStatus Code
GETRead/RetrieveGET /students/1200 OK
POSTCreatePOST /students201 Created
PUTReplace entirelyPUT /students/1200 OK
PATCHUpdate partiallyPATCH /students/1200 OK
DELETERemoveDELETE /students/1200 OK or 204
  • The method tells the server what action to perform
  • The URL tells the server which resource

RESTful URL Design

Bad (Verbs in URL)

GET /getStudents
POST /createStudent
GET /deleteStudent/1
POST /updateStudent/1

Good (Nouns only)

GET /students
POST /students
DELETE /students/1
PUT /students/1
  • URLs are nouns - they identify resources
  • HTTP methods are verbs - they specify actions
  • Use plural nouns - /students not /student

Resource Hierarchy in URLs

# Students collection
GET    /students              # List all students
POST   /students              # Create a student

# Single student resource
GET    /students/1            # Get student with ID 1
PUT    /students/1            # Update student with ID 1
DELETE /students/1            # Delete student with ID 1

# Nested resources (if needed)
GET    /students/1/courses    # Get courses for student 1
POST   /students/1/courses    # Add a course to student 1
  • Collections at the root: /students
  • Individual items by ID: /students/{id}
  • Related resources nested: /students/{id}/courses

Status Codes You'll Use Most

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH, DELETE
201CreatedSuccessful POST (resource created)
204No ContentSuccessful DELETE (nothing to return)
400Bad RequestInvalid data from client
404Not FoundResource doesn't exist
500Server ErrorSomething broke on your end
  • 2xx: Success - request worked
  • 4xx: Client error - they did something wrong
  • 5xx: Server error - you did something wrong

REST in Flask: Complete Example

from flask import Flask, request

app = Flask(__name__)
students = {}  # In-memory storage

@app.get('/students')
def list_students():
    return {"students": list(students.values())}

@app.post('/students')
def create_student():
    data = request.get_json()
    student = {"id": len(students) + 1, **data}
    students[student["id"]] = student
    return student, 201  # 201 = Created

@app.get('/students/<int:id>')
def get_student(id):
    if id not in students:
        return {"error": "Student not found"}, 404
    return students[id]

Part 4: Request & Response

Handling data in Flask

Where Data Can Come From

SourceExampleFlask Access
URL Path/students/42Function parameter
Query String?major=CS&limit=10request.args.get()
Request BodyJSON payloadrequest.get_json()
HeadersAuthorization: Bearer xyzrequest.headers.get()
  • URL Path: Resource identifiers (IDs)
  • Query String: Filtering, pagination, sorting
  • Body: Data for creating/updating resources
  • Headers: Authentication, content type

Getting JSON from Request Body

@app.post('/students')
def create_student():
    # Get JSON data from request body
    data = request.get_json()

    # data is now a Python dictionary
    name = data["name"]
    email = data["email"]

    # Create the student...
    return new_student, 201
  • request.get_json() parses JSON body into a dict
  • Requires Content-Type: application/json header
  • Returns None if no JSON or invalid JSON
Common mistake: Forgetting the Content-Type header in curl/Postman.

Query Parameters for Filtering

@app.get('/students')
def list_students():
    # Get optional query parameters
    major = request.args.get('major')  # ?major=CS
    limit = request.args.get('limit', default=10, type=int)

    results = list(students.values())

    # Filter by major if provided
    if major:
        results = [s for s in results if s.get("major") == major]

    # Apply limit
    results = results[:limit]

    return {"students": results, "total": len(results)}
  • request.args.get('key') returns query parameter or None
  • default= provides fallback value
  • type=int converts string to integer

URL Path Parameters

# Integer parameter
@app.get('/students/<int:student_id>')
def get_student(student_id):
    # student_id is already an int
    ...

# String parameter (default)
@app.get('/users/<username>')
def get_user(username):
    # username is a string
    ...

# Multiple parameters
@app.get('/courses/<course_code>/students/<int:student_id>')
def get_enrollment(course_code, student_id):
    ...
  • <name> captures string (default)
  • <int:name> captures and validates as integer
  • Multiple captures become multiple function parameters

Building Responses

# Simple: just return a dict (200 assumed)
return {"name": "Alice"}

# With status code
return {"name": "Alice"}, 200

# With status code (error)
return {"error": "Not found"}, 404

# With headers
return {"name": "Alice"}, 200, {"X-Custom-Header": "value"}

# Using jsonify (explicit)
from flask import jsonify
return jsonify({"name": "Alice"}), 200
  • Return dict - Flask converts to JSON with 200
  • Return tuple: (data, status_code)
  • Optional third element for custom headers

Part 5: Error Handling

Building robust APIs

Basic Error Pattern

@app.get('/students/<int:student_id>')
def get_student(student_id):
    student = students.get(student_id)
    if student is None:
        return {"error": "Student not found"}, 404
    return student, 200
  • Check for error conditions first
  • Return error response with appropriate status code
  • Only return success if everything is valid
Pattern: Validate early, fail fast, return clear error messages.

Using Flask's abort()

from flask import abort

@app.get('/students/<int:student_id>')
def get_student(student_id):
    student = students.get(student_id)
    if student is None:
        abort(404)  # Immediately returns 404 response
    return student
  • abort(status_code) immediately stops and returns error
  • Cleaner than nested if/else
  • Returns Flask's default error page (HTML by default)

Custom Error Handlers

@app.errorhandler(404)
def not_found(error):
    return {"error": "Resource not found"}, 404

@app.errorhandler(400)
def bad_request(error):
    return {"error": "Bad request"}, 400

@app.errorhandler(500)
def server_error(error):
    return {"error": "Internal server error"}, 500
  • @app.errorhandler(code) handles that status code
  • Now abort(404) returns JSON, not HTML
  • Consistent error format across your API

Validation Before Processing

@app.post('/students')
def create_student():
    data = request.get_json()

    # Validate required fields
    if not data:
        return {"error": "No JSON data provided"}, 400
    if "name" not in data:
        return {"error": "Missing required field: name"}, 400
    if "email" not in data:
        return {"error": "Missing required field: email"}, 400

    # Validate email format (simple check)
    if "@" not in data["email"]:
        return {"error": "Invalid email format"}, 400

    # Now safe to create student
    new_student = {"id": next_id, **data}
    students[next_id] = new_student
    return new_student, 201

Try/Except for Unexpected Errors

@app.post('/students')
def create_student():
    try:
        data = request.get_json()
        # ... validation and processing
        return new_student, 201
    except KeyError as e:
        return {"error": f"Missing field: {e}"}, 400
    except Exception as e:
        # Log the error for debugging
        app.logger.error(f"Error creating student: {e}")
        return {"error": "Internal server error"}, 500
  • Catch specific exceptions you expect
  • Log unexpected errors for debugging
  • Never expose internal error details to clients

Summary

Key takeaways from today

What You Learned Today

  • Flask is minimal: Your Python functions + decorators = API
  • The transformation: Week 2 functions → Week 3 Flask routes
  • RESTful design: Nouns in URLs, methods for actions
  • Data handling: request.get_json(), request.args, URL params
  • Error handling: Validate early, abort(), custom handlers
Key insight: You already knew 80% of this from Week 2. Flask just adds the HTTP layer.

Looking Ahead

WeekTopicBuilding On
4DockerContainerize your Flask app
6Validation & SchemasFlask-Smorest for data validation
7SQLAlchemy + MigrationsReplace in-memory with PostgreSQL
8Async Task QueuesBackground processing with Redis
Lab 2 (Friday): More Flask practice.
Prep for next week: O'Reilly Chapter 4 - Introduction to Docker
Note: Week 5 is Assignment 1 work week (no lecture)

Live Demo

From Week 2 Function to Flask API

Demo: What We'll Build

  • Start with Week 2's get_student() function
  • Add Flask imports and decorator
  • Run with flask run
  • Test with curl - see it work!
  • Add POST endpoint for creating students
Watch along: I'll type everything live. You'll practice in the in-class exploration.

In-Class Exploration

Student Records Flask API

Project Overview

  • Build a Flask API for student records
  • Same data structure from Week 2: id, name, email, major
  • Implement all CRUD endpoints:
    • GET /students - List all
    • GET /students/<id> - Get one
    • POST /students - Create
    • PUT /students/<id> - Update
    • DELETE /students/<id> - Delete
Goal: Transform Week 2's Python functions into a real Flask API!

Getting Started

  1. Click the GitHub Classroom link (in bCourses)
  2. Clone your repo:
    git clone <your-repo-url>
    cd in-class-exploration-week-3-<your-username>
  3. Create virtual environment and install Flask:
    python3 -m venv venv
    source venv/bin/activate  # On Windows: venv\Scripts\activate
    pip install -r requirements.txt
  4. Run the app:
    flask run

Testing Your API

# List all students
curl http://localhost:5000/students

# Get one student
curl http://localhost:5000/students/1

# Create a student
curl -X POST http://localhost:5000/students \
  -H "Content-Type: application/json" \
  -d '{"name": "New Student", "email": "new@berkeley.edu", "major": "CS"}'

# Update a student
curl -X PUT http://localhost:5000/students/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated Name"}'

# Delete a student
curl -X DELETE http://localhost:5000/students/1

Submitting (at 10:45 AM)

  1. Commit everything you have (even if incomplete):
    git add .
    git commit -m "In-class exploration submission"
  2. Push to your repository:
    git push
  3. Submit on bCourses: Add your GitHub repo URL
Grading: Pass/No Pass based on engagement. Just show you worked on it!

Want to keep working? Continue after submitting - just push additional changes.

Questions?

 

Website: groups.ischool.berkeley.edu/i253/sp26

Email: kay@ischool.berkeley.edu