Introduction: The Importance of Error Handling and Validation

Welcome back! In the last two lessons, you learned how to set up a FastAPI backend for a Code Review Assistant and how to manage changesets using API endpoints. Now, we will focus on making your API more reliable and secure by handling errors and validating user input.

Error handling and validation are essential for any web application. They help you:

  • Prevent bad data from entering your system.
  • Give clear feedback to users when something goes wrong.
  • Protect your application from crashes and security issues.

In this lesson, you will learn how to use FastAPI and Pydantic to validate incoming data and handle different types of errors gracefully. This will make your API safer and easier to use.

Quick Recall: FastAPI Endpoints and Pydantic Models

Before we dive in, let’s quickly remind ourselves how FastAPI endpoints and Pydantic models work together.

In FastAPI, you define endpoints using Python functions. When you want to receive structured data (like a changeset submission), you use a Pydantic model to describe what the data should look like.

For example, in a previous lesson, you might have seen something like this:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class ChangesetRequest(BaseModel):
    title: str
    author: str

@app.post("/api/changesets")
def submit_changeset(request: ChangesetRequest):
    return {"message": "Received", "title": request.title}

Here, ChangesetRequest is a Pydantic model that describes the expected fields. FastAPI automatically checks that incoming requests match this model and converts the data for you.

Validating Input with Pydantic

Now, let’s see how to add more detailed validation to our input data using Pydantic. This helps catch mistakes early and gives users helpful error messages.

Suppose we want to make sure that:

  • The file path is not empty and does not allow directory traversal (like ../).
  • The diff content is not empty and not too large.
  • The author field is a valid username or email.

We can use Pydantic’s @field_validator to add these checks.

Let’s start by defining a model for a file change:

from pydantic import BaseModel, field_validator

class FileChangeRequest(BaseModel):
    file_path: str
    diff_content: str

    @field_validator('file_path')
    @classmethod
    def validate_file_path(cls, v):
        if not v or len(v.strip()) == 0:
            raise ValueError('File path cannot be empty')
        if '..' in v or v.startswith('/'):
            raise ValueError('Invalid file path')
        return v.strip()

    @field_validator('diff_content')
    @classmethod
    def validate_diff_content(cls, v):
        if not v or len(v.strip()) == 0:
            raise ValueError('Diff content cannot be empty')
        if len(v) > 50000:  # 50KB limit
            raise ValueError('Diff content too large')
        return v

Explanation:

  • The @field_validator decorator lets you write custom checks for each field.
  • For file_path, we check that it is not empty and does not contain .. or start with /, which could be used to access files outside the allowed directory.
  • For diff_content, we check that it is not empty and not too large.

If a user sends invalid data, FastAPI will automatically return a helpful error message.

Handling Errors in FastAPI

Even with good validation, things can still go wrong. For example, the database might reject a request, or your code might raise a custom error. FastAPI lets you handle these situations using exception handlers.

Let’s look at how to handle three common types of errors:

  • Custom application errors
  • Validation errors
  • Database errors
1. Custom Application Errors

You can define your own exception class for special situations:

class CodeReviewException(Exception):
    def __init__(self, message: str, status_code: int = 500):
        self.message = message
        self.status_code = status_code
        super().__init__(self.message)

Then, you can tell FastAPI how to handle this error:

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(CodeReviewException)
async def code_review_exception_handler(request: Request, exc: CodeReviewException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"error": exc.message, "type": "code_review_error"}
    )

Now, if you raise CodeReviewException("Something went wrong", 400), the user will get a clear error message.

2. Validation Errors

FastAPI already handles validation errors for you, but you can customize the response:

from fastapi.exceptions import RequestValidationError
from fastapi import status

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={"error": "Invalid input data", "details": exc.errors()}
    )

This sends a clear message when the input data does not match your Pydantic model.

3. Database Errors

If you use a database, you might get errors like unique constraint violations. You can handle these too:

from sqlalchemy.exc import IntegrityError

@app.exception_handler(IntegrityError)
async def database_exception_handler(request: Request, exc: IntegrityError):
    return JSONResponse(
        status_code=status.HTTP_409_CONFLICT,
        content={"error": "Database constraint violation"}
    )

This way, your API always returns a clear and consistent error message.

Bringing It Together: Example Endpoint with Validation and Error Handling

Let’s see how all these pieces fit together in a real endpoint. Here’s how you might write an endpoint to submit a changeset, with validation and error handling:

from fastapi import BackgroundTasks, Depends

@app.post("/api/changesets", response_model=dict)
def submit_changeset(
    request: ChangesetRequest,
    background_tasks: BackgroundTasks,
    db: DummyDBSession = Depends(get_session)
):
    try:
        # Create changeset
        changeset = Changeset(
            title=request.title,
            description=request.description,
            author=request.author
        )
        db.add(changeset)
        db.flush()

        # Add files
        for file_req in request.files:
            changeset_file = ChangesetFile(
                changeset_id=changeset.id,
                file_path=file_req.file_path,
                diff_content=file_req.diff_content
            )
            db.add(changeset_file)

        db.commit()

        # Process review in background
        background_tasks.add_task(process_changeset_review, changeset.id)

        return {"message": "Changeset submitted", "changeset_id": changeset.id}

    except Exception as e:
        db.rollback()
        raise CodeReviewException("Failed to create changeset", 500)

Explanation:

  • The endpoint receives a ChangesetRequest (which uses Pydantic validation).
  • If anything goes wrong (for example, a database error), we roll back the transaction and raise a CodeReviewException.
  • The exception handlers we set up earlier will catch this and return a clear error message to the user.

Example Output:

  • If the request is valid, the response will be:
    {
      "message": "Changeset submitted",
      "changeset_id": 1
    }
  • If the input is invalid (for example, the file path is empty), the response will be:
    {
      "error": "Invalid input data",
      "details": [
        {
          "loc": ["body", "files", 0, "file_path"],
          "msg": "File path cannot be empty",
          "type": "value_error"
        }
      ]
    }
  • If there is a database error, the response will be:
    {
      "error": "Database constraint violation"
    }
Summary and What’s Next

In this lesson, you learned how to:

  • Use Pydantic to validate incoming data and provide helpful error messages.
  • Handle different types of errors in FastAPI using custom exception handlers.
  • Combine validation and error handling in a real API endpoint.

These skills will help you build APIs that are robust, secure, and user-friendly. Up next, you’ll get to practice these concepts with hands-on exercises. Good luck, and keep up the great work!

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