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:

  1. Exact path match (including trailing slash)
  2. Correct HTTP method (GET vs POST vs PUT, etc.)
  3. Path parameter types match (int vs str)
  4. Route actually registered (check /docs)
  5. 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:

  1. Visit /docs in your browser (FastAPI’s auto-generated documentation)
  2. Print app.routes on startup
  3. 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:

  1. Typo in route path or request URL
  2. Wrong HTTP method (GET vs POST vs PUT, etc.)
  3. Path parameter type mismatch (expecting int, got string)
  4. Trailing slash confusion
  5. APIRouter prefix issues
  6. Route order problems (generic before specific)

Remember to:

  • Define routes without trailing slashes (FastAPI convention)
  • Put specific routes before generic ones
  • Use /docs to 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.