Serving Your Personal Tutor with a RESTful API Using Flask

Welcome to the next step in our journey of building a personal tutor service with Flask. In the previous lesson, we focused on the TutorController, which manages tutoring sessions and handles student queries by interacting with both the model and service layers. Now, we will take a significant step forward by creating a RESTful API for our personal tutor service using Flask. We'll start by setting up the main Flask application, then adapt the TutorController to integrate with Flask's session management.

Understanding RESTful APIs and Flask

RESTful APIs are a way for different software systems to communicate over the internet. They provide a set of rules that allow programs to exchange data. Flask is a lightweight and flexible web framework for building APIs and web applications in Python. It is designed to be simple to use, making it a popular choice for building RESTful APIs.

To get started with Flask, you can install it using pip by running the following command in your terminal or command prompt:

pip install flask

Now, we can use Flask to connect the components we've already built, allowing students to interact with our personal tutor service through a web interface. This will enable seamless communication between students and our tutoring service.

Initializing a Flask App

First, we need to initialize the Flask application:

from flask import Flask

# Initialize Flask app
app = Flask(__name__)

Here, we import the Flask module and instantiate the Flask class to create our application object, app. This object will be used to configure and run our web application.

Adding Session Management with Flask

Flask provides built-in session management using secure cookies. To use sessions, you need to set a secret key for your application. This key is used to sign session cookies and keep session data secure.

# Set a secret key for session management
app.secret_key = "your_secret_key_here"

The secret_key is crucial for securing session data, as it's used to sign the session cookies to prevent tampering. Flask's session object allows you to store and retrieve data for each user's session.

Setting Up Static Files and Templates

For our personal tutor service, we want to provide a user-friendly interface. Flask serves static files (such as CSS, JavaScript, and images) from a directory named static by default and uses a directory named templates for HTML templates. Flask uses Jinja2 as its template engine, allowing you to render dynamic HTML pages.

from flask import render_template

# No additional setup is required for static files and templates if you use the default folders:
# - Place your static files in a folder named 'static'
# - Place your HTML templates in a folder named 'templates'

When you call render_template("tutor.html", ...), Flask will look for tutor.html in the templates directory.

Creating the Controller Instance

Next, we create an instance of our TutorController to handle the core operations of our personal tutor service:

from controllers.tutor_controller import TutorController

# Create an instance of TutorController to handle tutoring operations
tutor_controller = TutorController()

This controller will manage tutoring sessions and process student queries, serving as the bridge between our API endpoints and the underlying service layer. By instantiating it at the application level, we ensure it's available to all route handlers.

Managing Student Sessions

To ensure each student has a unique identifier, we use Flask's session object to check for a student ID and create one if it doesn't exist:

import uuid
from flask import session

def get_student_id():
    if "student_id" not in session:
        session["student_id"] = str(uuid.uuid4())
    return session["student_id"]

This function ensures that each student has a unique ID before processing their requests. The ID is stored in the session cookie and persists across requests from the same user.

Creating the Index Route

Now that we've initialized our Flask application and created our controller instance, let's create the API endpoints that will allow students to interact with our personal tutor service. In Flask, routes are defined using decorators that specify the HTTP method and path.

from flask import request, render_template

@app.route("/", methods=["GET"])
def index():
    student_id = get_student_id()
    return render_template("tutor.html")

This route handles GET requests to the root path (/). The @app.route("/", methods=["GET"]) decorator tells Flask that this function should be called when a user visits the homepage. The function ensures the student has an ID by calling get_student_id(), then returns a rendered HTML template, tutor.html.

Creating New Tutoring Sessions

Next, let's define a route for creating new tutoring sessions:

from flask import jsonify

@app.route("/api/create_session", methods=["POST"])
def create_session():
    student_id = get_student_id()
    response = tutor_controller.create_session(student_id)
    
    if isinstance(response, tuple):
        error_data = response[0]["error"]
        return jsonify({
            "status": "error",
            "error": {
                "message": error_data["message"],
                "code": error_data["code"]
            }
        }), error_data["code"]
    
    return jsonify(response["data"])

This route handles POST requests to /api/create_session. We use POST because we're creating a new resource (a tutoring session). The route uses the get_student_id() function to identify the student. The controller's create_session method returns either a success response or an error tuple. If it's an error, we return a JSON response with the appropriate status code and message; otherwise, we return the data part of the response.

Handling Student Queries

Now, let's create a route for sending queries to the tutor:

@app.route("/api/send_query", methods=["POST"])
def send_query():
    student_id = get_student_id()
    data = request.get_json()
    session_id = data.get("session_id")
    query = data.get("query")
    
    response = tutor_controller.send_query(
        student_id,
        session_id,
        query
    )
    
    if isinstance(response, tuple):
        error_data = response[0]["error"]
        return jsonify({
            "status": "error",
            "error": {
                "message": error_data["message"],
                "code": error_data["code"]
            }
        }), error_data["code"]
    
    return jsonify(response["data"])

This route handles POST requests to /api/send_query. It expects a JSON request body containing session_id and query. The controller's send_query method processes the query and returns either a success response or an error tuple, which we handle similarly to the create_session route.

Custom Exception Handling

To ensure consistent error responses across our API, we can add a custom error handler for HTTP errors:

from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def handle_http_exception(e):
    response = e.get_response()
    response.data = jsonify({
        "status": "error",
        "error": {
            "message": e.description,
            "code": e.code
        }
    }).data
    response.content_type = "application/json"
    return response

This handler ensures that all HTTP exceptions are converted to JSON responses with a consistent structure, making it easier for clients to handle errors.

Running the Flask Server

Now that we've defined our routes and prepared our controller for integration with Flask, the final step is to set up the server to run our application. We'll use Flask's built-in development server.

if __name__ == "__main__":
    app.run(
        host="0.0.0.0",
        port=3000,
        debug=True
    )

This code block does several important things:

  1. The if __name__ == "__main__": condition ensures that the server only starts when we run this file directly, not when it's imported by another module.

  2. The app.run() function launches the Flask development server with our application. The host is set to "0.0.0.0" to make the server accessible from any network interface, not just localhost. The port is set to 3000, so our application will be available at http://localhost:3000.

  3. The debug=True parameter enables debug mode, which automatically restarts the server when you make changes to your code. This is extremely helpful during development.

Adapting the TutorController

The TutorController has been designed to work seamlessly with Flask, handling student IDs and session management effectively. Let's examine its key methods and how they integrate with our API.

class TutorController:
    def __init__(self):
        self.tutor_service = TutorService()
    
    def create_session(self, student_id):
        if not student_id:
            return self._error_response('Session expired', 401)
        
        session_id = self.tutor_service.create_session(student_id)
        
        return self._success_response({
            'session_id': session_id,
            'message': 'Tutoring session created successfully'
        })

The create_session method accepts a student_id parameter from Flask's session management. It checks if the student ID is valid, creates a new tutoring session using the TutorService, and returns a success response with the session ID.

    def send_query(self, student_id, session_id, student_query):
        if not student_id:
            return self._error_response('Session expired', 401)
        
        if not session_id or not student_query:
            return self._error_response('Missing session_id or query', 400)
            
        try:
            tutor_response = self.tutor_service.process_query(
                student_id, 
                session_id, 
                student_query
            )
            return self._success_response({'message': tutor_response})
            
        except ValueError as e:
            return self._error_response(str(e), 404)
        except RuntimeError as e:
            return self._error_response(str(e), 500)

The send_query method is responsible for processing student queries. It receives the student ID, session ID, and the query content. After validating these parameters, it calls the process_query method on the TutorService to generate a response. The method includes comprehensive error handling, returning appropriate error responses for various scenarios.

    def _success_response(self, data):
        response = {
            'status': 'success',
            'data': data
        }
        return response
    
    def _error_response(self, message, status_code):
        response = {
            'status': 'error',
            'error': {
                'message': message,
                'code': status_code
            }
        }
        return response, status_code

The controller also includes helper methods for generating consistent success and error responses. The _success_response method wraps data in a standard structure, while the _error_response method creates a tuple containing the error details and the appropriate HTTP status code. This consistent response format makes it easier for our Flask routes to handle the controller's output.

How Students Interact with the Personal Tutor API

To interact with our personal tutor service, a student can follow these steps:

  1. Access the Index Route (/): The student begins by accessing the index route of the API. This step ensures that a student session is established. A student session is a way to keep track of the student's interactions with the service. It is stored using Flask's session management, which utilizes cookies to maintain session data on the client side. The server responds with the tutor interface, ready for interaction.

  2. Create a Tutoring Session (/api/create_session): Before sending a query, the student needs to create a new tutoring session. This is done by sending a request to the /api/create_session route. The server will respond with a unique session ID, which is essential for identifying the tutoring session in subsequent interactions. The student session ensures that the tutoring session is associated with the correct student, allowing for personalized academic support.

  3. Send a Query (/api/send_query): With the tutoring session established, the student can now send academic questions to the personal tutor. This involves sending a request to the /api/send_query route, including the session ID and the query content. The server processes the query and responds with the tutor's explanation, allowing the student to continue the learning conversation. The student session helps maintain continuity in the conversation by linking the queries to the correct student and tutoring session.

By following these steps, a student can effectively receive personalized academic support from our personal tutor service, leveraging the RESTful API to manage tutoring sessions and exchange queries while utilizing student sessions for a seamless learning experience.

Summary and Next Steps

In this lesson, we successfully built a RESTful API for our personal tutor service using Flask. We set up a Flask application, defined routes for tutoring operations, and explored how the TutorController integrates with Flask's session management. This lesson marks a significant milestone in our course, as it brings together all the components we've developed so far into a functional web application that can deliver personalized academic support at scale.

As you move on to the practice exercises, take the opportunity to experiment with the Flask API and reinforce the concepts covered in this lesson. This hands-on practice will prepare you for further development and exploration of additional Flask features and RESTful API concepts. Keep up the great work, and I look forward to seeing your progress!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal