Path parameters enable retrieving specific resources from your data by capturing variable parts of the URL. This chapter demonstrates creating both API endpoints and template pages for individual posts, implementing proper HTTP status codes, building comprehensive error handling that serves JSON to API clients and HTML to browser users, and leveraging FastAPI’s automatic type validation.
Understanding Path Parameters
Path parameters are variable parts of the URL path. Instead of a route like /api/posts that returns all posts, a route like /api/posts/1 or /api/posts/2 uses the number as a parameter to retrieve a specific post. FastAPI captures this value from the URL and passes it to the route function as an argument. Critical capability: FastAPI automatically validates the parameter based on the type hint in the function signature.
Creating a Single Post API Endpoint
Begin with a basic endpoint that retrieves one post by ID:
@app.get("/api/posts/{post_id}")
def get_post(post_id: int):
for post in posts:
if post.get("id") == post_id:
return post
return {"error": "Post not found"}How this works: The {post_id} in the route path tells FastAPI this is a variable that should be captured. In the function signature, post_id: int declares the parameter with a type hint of integer. This type hint is crucial—FastAPI uses it to automatically validate input. If someone accesses /api/posts/hello, FastAPI returns a validation error because “hello” is not an integer. The function loops through posts, comparing each dictionary’s ID to the path parameter. When a match is found, that post is returned.
Accessing /api/posts/1 returns only the post with ID 1. /api/posts/2 returns the post with ID 2. For a non-existent post like /api/posts/99, the error dictionary is returned.
Proper HTTP Status Codes
The current implementation has a critical flaw. When accessing /api/posts/99 (a non-existent post), the development server logs show a 200 status code—indicating success. This is incorrect. A 200 response tells clients the request succeeded, but the post wasn’t found, so a 404 (Not Found) status code should be returned instead.
Why this matters: Client applications rely on status codes to understand how to handle responses. A 200 suggests everything is fine. A 404 signals the resource doesn’t exist, allowing the client to handle this appropriately. This is a RESTful API best practice.
Using HTTPException
Import the necessary components:
from fastapi import FastAPI, Request, HTTPException, statusHTTPException enables returning proper HTTP error responses. The status module provides constants for HTTP status codes, making code more readable. Instead of the magic number 404, use status.HTTP_404_NOT_FOUND, which makes the intent explicit. This becomes especially valuable for less familiar codes—status.HTTP_422_UNPROCESSABLE_ENTITY is far clearer than the raw number 422.
Update the endpoint to raise an exception instead of returning an error dictionary:
@app.get("/api/posts/{post_id}")
def get_post(post_id: int):
for post in posts:
if post.get("id") == post_id:
return post
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)When no matching post is found, the function raises HTTPException with a 404 status code and a detail message explaining why. FastAPI automatically handles this exception and returns the appropriate response to the client.
Now accessing /api/posts/99 still returns {"detail": "Post not found"}, but the development server logs show a 404 status code instead of 200. This correct status code enables reliable API consumption.
Automatic Type Validation
Type hints provide powerful automatic validation. With post_id: int declared, accessing /api/posts/hello triggers a 422 (Unprocessable Entity) response with a detailed error message explaining that the input should be a valid integer and is unable to parse the string as an integer:
/attachments/Pasted-image-20260319233322.png)
No additional code was written for this validation. FastAPI handles it automatically based on the type hint. This validation also appears in the automatic documentation at /docs. The documentation shows that post_id must be an integer, and the interactive interface prevents submitting non-integer values.
Creating a Single Post Template Page
While the API endpoint returns JSON, a separate template route serves HTML for browser users viewing individual posts.
Create post.html in the templates directory with the appropriate code (look in the repo).
The template extends the layout, displays author information and post metadata, and includes placeholder buttons for edit and delete functionality that will be implemented when authentication is added later in the series.
Add the corresponding route:
@app.get("/posts/{post_id}", include_in_schema=False)
def post_page(request: Request, post_id: int):
for post in posts:
if post.get("id") == post_id:
title = post["title"][:50] # Truncate long titles
return templates.TemplateResponse(
request,
"post.html",
{
"post": post,
"title": title
}
)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)Key differences from the API endpoint: The path is /posts/{post_id} instead of /api/posts/{post_id}. The include_in_schema=False parameter excludes this from API documentation since it returns HTML, not JSON. The function signature includes request: Request because Jinja2 templates require it. Instead of returning the post directly, it returns a TemplateResponse with the post as a single object (not a list) in the context. The title is truncated to 50 characters for cases where post titles are excessively long.
Making Posts Clickable
Update home.html to link post titles to their individual pages:
<a class="article-title" href="{{ url_for("post_page", post_id=post.id) }}">{{ post.title }}</a>The url_for('post_page', post_id=post.id) generates URLs like /posts/1 and /posts/2. Path parameters are passed as keyword arguments to url_for(). This approach is powerful because changing the URL structure in main.py automatically updates all template links—no manual find-and-replace required.
Comprehensive Error Handling
The current error handling has a significant problem. Accessing /posts/99 (a non-existent post) in the browser returns raw JSON: {"detail": "Post not found"}. While JSON is appropriate for API clients, browser users should see a proper HTML error page. However, API clients requesting /api/posts/99 must still receive JSON.
The solution: Handle errors differently based on whether the request is for the API or the website. If the request path starts with /api, return JSON. Otherwise, return an HTML error page.
Creating an Error Template
Create error.html with the proper code. This simple template extends the layout and displays the status code and error message.
Exception Handler Setup
Import the required components:
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPExceptionWhy import HTTPException from Starlette? FastAPI is built on top of Starlette. When a user visits a non-existent route like /doesnotexist, Starlette raises the 404 error, not FastAPI. If you only catch FastAPI’s HTTPException, you handle manual errors from your code but miss automatic 404 errors from Starlette. By also catching StarletteHTTPException, both cases are covered. The import is aliased to avoid naming conflicts with FastAPI’s HTTPException.
RequestValidationError handles validation errors (like passing “hello” when an integer is expected). JSONResponse allows manually returning JSON responses from exception handlers.
HTTP Exception Handler
Add an exception handler after all route definitions:
@app.exception_handler(StarletteHTTPException)
def http_exception_handler(request: Request, exception: StarletteHTTPException):
message = (
exception.detail
if exception.detail
else "An error occurred. Please check your request and try again."
)
# For API routes, return JSON
if request.url.path.startswith("/api"):
return JSONResponse(
status_code=exception.status_code,
content={"detail": message}
)
# For template routes, return HTML
return templates.TemplateResponse(
request,
"error.html",
{
"status_code": exception.status_code,
"message": message,
"title": exception.status_code
},
status_code=exception.status_code
)The @app.exception_handler(StarletteHTTPException) decorator tells FastAPI to use this function for all Starlette HTTP exceptions. The handler receives the request and the exception. It first sets a message from exception.detail if available, or uses a default message for cases where Starlette raises an exception without detail. If the request URL path starts with /api, it returns a JSONResponse with the status code and message. Otherwise, it returns the error template with the status code, message, and title in the context.
Critical detail: The status_code=exception.status_code parameter in TemplateResponse ensures the correct HTTP status code is sent to the browser. Without this, the template would display as an error but return a 200 (success) status code.
Note on the manual JSONResponse: This simplified version works well for current needs. Later, when introducing asynchronous functions, FastAPI’s built-in exception handlers will be used instead, as they automatically handle edge cases like custom headers that this manual approach skips.
Validation Error Handler
Add a second handler for validation errors:
@app.exception_handler(RequestValidationError)
def validation_exception_handler(request: Request, exc: RequestValidationError):
# For API routes, return detailed validation errors in JSON
if request.url.path.startswith("/api"):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": exc.errors()}
)
# For template routes, return simple HTML error
return templates.TemplateResponse(
request,
"error.html",
{
"status_code": 422,
"message": "Invalid request. Please check your input.",
"title": 422
},
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
)Key differences from HTTP exception handling: Validation errors are a different exception type—RequestValidationError, not HTTPException. Validation errors don’t have a simple detail string; they have a list of detailed error information including the field, the problem, and the input value. For API responses, exc.errors() is passed in the detail, allowing developers consuming the API to see exactly what went wrong. For HTML responses, technical validation details are not exposed to end users—they simply see “Invalid request.” Validation errors don’t have a status_code attribute; they are always 422 errors, so status.HTTP_422_UNPROCESSABLE_ENTITY is used everywhere.
Testing Error Handling
Manual 404 on a template route: Accessing /posts/99 now displays the error template with “Oops! 404 Error - Post not found” instead of raw JSON. Browser users see a friendly error page.
Automatic 404 for non-existent pages: Accessing /doesnotexist also displays the error template with “404 - Not found.” This is the default message from Starlette since no custom detail was provided.
API routes still return JSON: Accessing /api/posts/99 returns {"detail": "Post not found"} in JSON format. The separation works correctly.
Validation errors: Accessing /api/posts/hello returns detailed JSON validation errors with a 422 status code. Accessing /posts/hello in the browser displays the HTML error template with “422 - Invalid request.”
Separation of Concerns
The architecture now has clear separation:
- API endpoints (under
/api) return JSON for programmatic access by front-end JavaScript applications, mobile apps, or any service consuming the API. - Template routes (like
/posts) return HTML pages for humans browsing the website—a custom-built frontend, not general API consumption. - Both use the same data source. Currently, this is an in-memory list, but it will become a database later. The same underlying data powers both the API and the HTML frontend.
Summary
This chapter introduced path parameters for accessing individual resources. Path parameters capture variable URL parts using curly braces ({post_id}), are automatically validated based on type hints in function signatures, and can be passed to url_for() as keyword arguments for dynamic link generation. Proper HTTP status codes were implemented using HTTPException with status constants. A comprehensive error handling system was built that serves JSON errors to API clients and HTML error pages to browser users, catches both FastAPI exceptions and Starlette exceptions for complete coverage, and handles both HTTP exceptions (404, etc.) and validation errors (422) appropriately.
The application now has a solid foundation with clear API/frontend separation. Currently using dummy data, the next chapter will introduce Pydantic models and schemas to add real validation and structure to the data, setting the stage for transitioning from the in-memory list to a real database.