You’ve defined your FastAPI routes, your server’s running, but when you hit the endpoint, you get a 404 Not Found. The route looks correct, the URL seems right, but FastAPI just won’t find it. We’ve all been there, and it’s frustrating because the error message doesn’t give you much to work with.
TLDR: Quick Fix for 404 Not Found
Most Common Cause: Route path doesn’t match the URL you’re requesting - check for typos, trailing slashes, or missing path parameters.
❌ BAD (causes 404):
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users") # Note: exact path
def get_users():
return {"users": ["Alice", "Bob"]}
# Request to /api/users/ (with trailing slash) → 404
# Request to /api/user (typo) → 404
# Request to /API/users (wrong case) → 404
✅ GOOD (fixes it):
from fastapi import FastAPI
app = FastAPI()
# FastAPI automatically redirects /api/users/ to /api/users
@app.get("/api/users")
def get_users():
return {"users": ["Alice", "Bob"]}
# Request to /api/users → 200 OK
# Request to /api/users/ → 307 redirect to /api/users, then 200 OK
Other Common Causes:
- Wrong HTTP method (GET vs POST)
- Path parameter type mismatch
- Route defined after middleware that blocks it
- Missing APIRouter prefix
- Route registered to wrong app instance
What is a 404 Not Found Error?
A 404 Not Found error means FastAPI couldn’t find a route that matches your request. When FastAPI receives a request, it tries to match the URL path and HTTP method against all registered routes. If nothing matches, it returns a 404.
Here’s what a typical 404 response looks like:
{
"detail": "Not Found"
}
That’s it. No hints about what went wrong, which makes debugging tricky. But don’t worry - we’ll walk through the most common causes and how to fix them.
Why Does This Error Happen?
FastAPI is extremely strict about route matching. The URL path must match exactly, including:
- The exact path string
- The HTTP method (GET, POST, PUT, DELETE, etc.)
- Path parameter types
- Trailing slashes (with some automatic handling)
Even a tiny difference - a typo, wrong case, or missing slash - results in a 404. This strictness is actually good because it prevents ambiguous routing, but it means you need to be precise when defining routes.
Common 404 Error Scenarios
1. Typo in Route Path
The most obvious cause - you misspelled something in either the route definition or the request URL.
❌ BAD (route and request don’t match):
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users")
def get_users():
return {"users": ["Alice", "Bob"]}
# Request to /api/user (missing 's') → 404 Not Found
# Request to /api/userz (typo) → 404 Not Found
# Request to /api/Users (wrong case) → 404 Not Found
✅ GOOD (exact match):
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users")
def get_users():
return {"users": ["Alice", "Bob"]}
# Request to /api/users → 200 OK
Pro tip: URLs are case-sensitive. /api/users and /api/Users are different routes.
2. Wrong HTTP Method
You defined a POST route but you’re sending a GET request (or vice versa). This is surprisingly common when testing with browsers or curl.
❌ BAD (method mismatch):
from fastapi import FastAPI
app = FastAPI()
# Route only accepts POST requests
@app.post("/api/users")
def create_user(name: str):
return {"name": name}
# GET request to /api/users → 405 Method Not Allowed (not 404!)
# But if you have no GET route, it shows as 404 in some clients
✅ GOOD (define both methods if needed):
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users")
def get_users():
return {"users": ["Alice", "Bob"]}
@app.post("/api/users")
def create_user(name: str):
return {"name": name}
# GET /api/users → 200 OK (gets list)
# POST /api/users → 200 OK (creates user)
Actually, if you send the wrong method to a route that exists, FastAPI returns 405 Method Not Allowed, not 404. But this distinction can be confusing, so always double-check you’re using the right HTTP method.
3. Trailing Slash Issues
FastAPI has special handling for trailing slashes, but it can still trip you up. By default, FastAPI automatically redirects /path/ to /path with a 307 redirect.
❌ BAD (can cause confusion):
from fastapi import FastAPI
app = FastAPI()
# Route defined WITHOUT trailing slash
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
# Request to /api/users → 200 OK
# Request to /api/users/ → 307 redirect to /api/users, then 200 OK
# This usually works, but some clients don't follow redirects!
✅ GOOD (be consistent):
from fastapi import FastAPI
app = FastAPI()
# Define routes without trailing slash (FastAPI convention)
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
# Always request without trailing slash
# GET /api/users → 200 OK
Best practice: Don’t use trailing slashes in FastAPI routes. Let FastAPI handle redirects automatically, or disable redirect behavior if you need strict matching.
To disable automatic redirect:
from fastapi import FastAPI
app = FastAPI(redirect_slashes=False)
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
# Now /api/users/ → 404 (no redirect)
4. Missing or Wrong Path Parameters
Path parameters must match the type specified in your route. If they don’t, FastAPI can’t match the route.
❌ BAD (type mismatch):
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int): # Expects an integer
return {"user_id": user_id}
# Request to /users/123 → 200 OK
# Request to /users/abc → 404 Not Found (can't convert 'abc' to int)
# This isn't a 422 error because FastAPI can't even match the route!
✅ GOOD (correct type):
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# Request to /users/123 → 200 OK
If you need to accept non-integer IDs, use str:
✅ BETTER (flexible):
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: str): # Accepts any string
return {"user_id": user_id}
# Request to /users/123 → 200 OK
# Request to /users/abc → 200 OK
# Request to /users/user-123 → 200 OK
5. Route Order Matters
If you have overlapping routes, FastAPI matches the first one it finds. More specific routes should come before generic ones.
❌ BAD (generic route shadows specific ones):
from fastapi import FastAPI
app = FastAPI()
# Generic route defined first
@app.get("/users/{user_id}")
def get_user(user_id: str):
return {"user_id": user_id}
# Specific route defined second - NEVER REACHED!
@app.get("/users/me")
def get_current_user():
return {"user": "current"}
# Request to /users/me → matches first route with user_id="me"
# The /users/me route is never reached!
✅ GOOD (specific routes first):
from fastapi import FastAPI
app = FastAPI()
# Specific route defined first
@app.get("/users/me")
def get_current_user():
return {"user": "current"}
# Generic route defined second
@app.get("/users/{user_id}")
def get_user(user_id: str):
return {"user_id": user_id}
# Request to /users/me → matches first route (correct!)
# Request to /users/123 → matches second route
Rule of thumb: Define literal paths before parameterized paths.
6. APIRouter Prefix Issues
When using APIRouter, forgetting to include the prefix or including it twice causes 404s.
❌ BAD (prefix confusion):
from fastapi import FastAPI, APIRouter
app = FastAPI()
router = APIRouter(prefix="/api")
# Route path includes prefix - WRONG!
@router.get("/api/users")
def get_users():
return {"users": ["Alice"]}
app.include_router(router)
# Request to /api/users → 404 (actual route is /api/api/users)
# Request to /api/api/users → 200 OK (but this is wrong!)
✅ GOOD (correct prefix usage):
from fastapi import FastAPI, APIRouter
app = FastAPI()
router = APIRouter(prefix="/api")
# Route path without prefix - let router add it
@router.get("/users")
def get_users():
return {"users": ["Alice"]}
app.include_router(router)
# Request to /api/users → 200 OK (correct!)
Or don’t use a prefix on the router:
✅ ALSO GOOD (explicit paths):
from fastapi import FastAPI, APIRouter
app = FastAPI()
router = APIRouter() # No prefix
# Full path in route
@router.get("/api/users")
def get_users():
return {"users": ["Alice"]}
app.include_router(router)
# Request to /api/users → 200 OK
7. Multiple APIRouter Tags/Includes
If you include the same router multiple times with different prefixes, or forget to include it at all, routes won’t be registered where you expect.
❌ BAD (router not included):
from fastapi import FastAPI, APIRouter
app = FastAPI()
router = APIRouter(prefix="/api")
@router.get("/users")
def get_users():
return {"users": ["Alice"]}
# Forgot to include router!
# app.include_router(router)
# Request to /api/users → 404 (router never registered)
✅ GOOD (router included):
from fastapi import FastAPI, APIRouter
app = FastAPI()
router = APIRouter(prefix="/api")
@router.get("/users")
def get_users():
return {"users": ["Alice"]}
app.include_router(router) # Don't forget this!
# Request to /api/users → 200 OK
8. Mounting Sub-Applications
When mounting sub-applications, the path must match the mount point.
❌ BAD (wrong mount path):
from fastapi import FastAPI
app = FastAPI()
sub_app = FastAPI()
@sub_app.get("/users")
def get_users():
return {"users": ["Alice"]}
# Mount sub-app at /api
app.mount("/api", sub_app)
# Request to /users → 404 (not mounted there)
# Request to /api → 404 (need full path)
# Request to /api/users → 200 OK (correct!)
✅ GOOD (understand mount paths):
from fastapi import FastAPI
app = FastAPI()
sub_app = FastAPI()
@sub_app.get("/users")
def get_users():
return {"users": ["Alice"]}
app.mount("/api", sub_app)
# The sub_app route is at /users
# But it's mounted at /api
# So the full path is /api + /users = /api/users
# Request to /api/users → 200 OK
Debugging 404 Errors
1. List All Routes
FastAPI can show you all registered routes. This is the best debugging tool.
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
@app.get("/api/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# Print all routes when app starts
@app.on_event("startup")
def list_routes():
print("\n=== Registered Routes ===")
for route in app.routes:
if hasattr(route, "methods"):
print(f"{list(route.methods)[0]:7} {route.path}")
print("========================\n")
When you run this, you’ll see:
=== Registered Routes ===
GET /api/users
GET /api/users/{user_id}
========================
Now you can compare this list with the URL you’re trying to reach.
2. Enable Debug Mode
Run FastAPI with debug logging to see what’s happening:
import logging
from fastapi import FastAPI, Request
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.debug(f"Request: {request.method} {request.url.path}")
response = await call_next(request)
logger.debug(f"Response: {response.status_code}")
return response
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
This logs every request and shows exactly what path FastAPI received.
3. Use FastAPI’s Interactive Docs
FastAPI automatically generates interactive API documentation at /docs. Open your browser to http://localhost:8000/docs and you’ll see all registered routes with their exact paths and methods.
This is incredibly helpful for verifying:
- What routes are actually registered
- What parameters they expect
- What HTTP methods they support
4. Test with curl
Test your endpoints with curl to eliminate browser or frontend issues:
# Test GET request
curl -X GET "http://localhost:8000/api/users" -v
# Test POST request
curl -X POST "http://localhost:8000/api/users" \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}' -v
# The -v flag shows full request and response headers
5. Check for Typos Systematically
Compare your route definition and request URL character by character:
# Route definition:
@app.get("/api/users")
# Request URL:
# /api/users ✓
# /api/user ✗ (missing 's')
# /api/users/ ✓ (FastAPI redirects)
# /API/users ✗ (wrong case)
# /api/userss ✗ (extra 's')
6. Verify Path Parameter Types
If your route expects an int, make sure you’re sending an integer:
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# Test these URLs:
# /users/123 ✓ (valid int)
# /users/abc ✗ (not an int → 404)
# /users/12.5 ✗ (not an int → 404)
Custom 404 Handlers
Override Default 404 Response
Customize the 404 error message to be more helpful:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import HTTPException
app = FastAPI()
@app.exception_handler(404)
async def custom_404_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=404,
content={
"error": "Not Found",
"message": f"The path {request.url.path} was not found",
"available_routes": [
route.path for route in app.routes
if hasattr(route, "path")
]
}
)
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
Now when you hit a 404, you get a helpful response:
{
"error": "Not Found",
"message": "The path /api/user was not found",
"available_routes": ["/api/users", "/docs", "/openapi.json"]
}
Catch-All Route for SPAs
If you’re serving a Single Page Application (SPA), you might want to redirect all unmatched routes to index.html:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
app = FastAPI()
# Your API routes
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
# Serve static files
app.mount("/static", StaticFiles(directory="static"), name="static")
# Catch-all for SPA routing (must be last!)
@app.get("/{full_path:path}")
def serve_spa(full_path: str):
# Don't catch API routes
if full_path.startswith("api/"):
return {"detail": "Not Found"}
# Return index.html for all other routes
return FileResponse("static/index.html")
Common Patterns and Solutions
Pattern 1: Versioned API Routes
from fastapi import FastAPI, APIRouter
app = FastAPI()
# Version 1 routes
v1_router = APIRouter(prefix="/api/v1", tags=["v1"])
@v1_router.get("/users")
def get_users_v1():
return {"users": ["Alice"], "version": "1"}
# Version 2 routes
v2_router = APIRouter(prefix="/api/v2", tags=["v2"])
@v2_router.get("/users")
def get_users_v2():
return {"users": ["Alice", "Bob"], "version": "2"}
app.include_router(v1_router)
app.include_router(v2_router)
# /api/v1/users → version 1
# /api/v2/users → version 2
Pattern 2: Nested Resources
from fastapi import FastAPI
app = FastAPI()
# Users
@app.get("/users")
def get_users():
return {"users": ["Alice", "Bob"]}
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# User's posts (nested resource)
@app.get("/users/{user_id}/posts")
def get_user_posts(user_id: int):
return {"user_id": user_id, "posts": []}
@app.get("/users/{user_id}/posts/{post_id}")
def get_user_post(user_id: int, post_id: int):
return {"user_id": user_id, "post_id": post_id}
Pattern 3: Optional Trailing Segments
from fastapi import FastAPI
app = FastAPI()
# Match both /files and /files/subpath
@app.get("/files/{file_path:path}")
def get_file(file_path: str = ""):
if not file_path:
return {"message": "List all files"}
return {"file_path": file_path}
# /files → file_path=""
# /files/docs → file_path="docs"
# /files/docs/readme.md → file_path="docs/readme.md"
Pattern 4: Route Groups with Dependencies
from fastapi import APIRouter, Depends
def verify_token(token: str):
if token != "secret":
raise HTTPException(403, "Invalid token")
return token
# Public routes (no auth)
public_router = APIRouter(prefix="/public")
@public_router.get("/info")
def public_info():
return {"info": "This is public"}
# Protected routes (require auth)
protected_router = APIRouter(
prefix="/protected",
dependencies=[Depends(verify_token)]
)
@protected_router.get("/data")
def protected_data():
return {"data": "This is protected"}
app = FastAPI()
app.include_router(public_router)
app.include_router(protected_router)
# /public/info → 200 OK (no auth)
# /protected/data → 403 Forbidden (no token)
# /protected/data?token=secret → 200 OK (valid token)
Testing Routes
Using TestClient
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/api/users")
def get_users():
return {"users": ["Alice"]}
client = TestClient(app)
def test_get_users():
response = client.get("/api/users")
assert response.status_code == 200
assert response.json() == {"users": ["Alice"]}
def test_not_found():
response = client.get("/api/user") # Typo
assert response.status_code == 404
def test_wrong_method():
response = client.post("/api/users")
# Might be 405 if POST route exists, or 404 if not
assert response.status_code in [404, 405]
Using pytest
import pytest
from fastapi.testclient import TestClient
from main import app
@pytest.fixture
def client():
return TestClient(app)
def test_routes_exist(client):
"""Test that all expected routes return non-404"""
routes = [
"/api/users",
"/api/users/1",
"/api/posts",
]
for route in routes:
response = client.get(route)
assert response.status_code != 404, f"Route {route} returned 404"
Frequently Asked Questions
Why am I getting 404 when the route looks correct?
Check these in order:
- Exact path match (including trailing slash)
- Correct HTTP method (GET vs POST vs PUT, etc.)
- Path parameter types match (int vs str)
- Route actually registered (check
/docs) - No typos in either route or request
What’s the difference between 404 and 405?
- 404 Not Found: No route matches the path at all
- 405 Method Not Allowed: Route exists but doesn’t support the HTTP method you used
For example, if you have @app.post("/users") and you send GET to /users, that’s a 405, not 404.
Do I need to include trailing slashes in FastAPI routes?
No. FastAPI convention is to define routes without trailing slashes. FastAPI automatically redirects requests with trailing slashes to the version without, so both work (with a 307 redirect for the trailing slash version).
How do I see all registered routes?
Three ways:
- Visit
/docsin your browser (FastAPI’s auto-generated documentation) - Print
app.routeson startup - Use
fastapi.routing.get_routes(app)to programmatically list routes
Can I use regex patterns in route paths?
Not directly in the path string, but you can use path parameters with validation:
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
def get_item(item_id: str = Path(..., regex="^[a-z0-9-]+$")):
return {"item_id": item_id}
This validates the parameter format but doesn’t affect route matching for 404 purposes.
Why does my catch-all route match everything?
Catch-all routes with {path:path} match any URL. They should be defined last:
# Specific routes first
@app.get("/api/users")
def get_users():
pass
# Catch-all last
@app.get("/{path:path}")
def catch_all(path: str):
pass
Summary
The 404 Not Found error in FastAPI happens when no route matches your request. The most common causes are:
- Typo in route path or request URL
- Wrong HTTP method (GET vs POST vs PUT, etc.)
- Path parameter type mismatch (expecting int, got string)
- Trailing slash confusion
- APIRouter prefix issues
- Route order problems (generic before specific)
Remember to:
- Define routes without trailing slashes (FastAPI convention)
- Put specific routes before generic ones
- Use
/docsto verify registered routes - Check path parameter types match
- Include APIRouter with
app.include_router() - Test with curl to eliminate frontend issues
FastAPI’s automatic documentation at /docs is your best friend for debugging 404s. It shows exactly which routes are registered and what they expect.
Related Posts:
Debugging Stack Traces? Use Debugly’s trace formatter to quickly parse and analyze Python tracebacks.