I have a FastAPI + Next.js project deployed on Vercel. Locally everything works, but on Vercel some routes break depending on whether I use a trailing slash in the route definition.
For example, in my usrs.py router:
from fastapi import APIRouter, Depends
from ..schemas.user import UserOut
from ..auth import require_admin
from ..repositories.users import list_users
user_router = APIRouter(prefix="/users", tags=["users"])
@user_router.get("/", response_model=list[UserOut])
def get_users(_: UserOut = Depends(require_admin)):
return list_users()
Locally (Uvicorn)
GET http://localhost:8001/api/users/ works.
GET http://localhost:8001/api/users redirects to /users/ as expected.
On Vercel (serverless FastAPI):
GET https://myapp.vercel.app/api/users/ → 500
GET https://myapp.vercel.app/api/users 404 or .vercel.app/api/users/ net::ERR_TOO_MANY_REDIRECTS
So I tried removing the slash in the route definition:
@user_router.get("", response_model=list[UserOut])
def get_users(_: UserOut = Depends(require_admin)):
return list_users()
Now it works on Vercel, but fails locally (404).
What I’ve checked
DB and schema are fine (/api/db-check and /api/_schema_status return OK).
Other endpoints like /api/healthz and /api/users/login work fine.
My next.config.js proxies /api/:path* → FastAPI in dev, and Vercel vercel.json routes /api/(.*) → /api/index.py.
Question
I resolved this issue. I've run into the "tailing-slash" mismatch between FastAPI/Starlette and the way Vercel normalize paths.
Locally: FastAPI' default RedirectSlashes middleware happily 307-redirects /api/users ->/api/users/ . My route with @get("/") then matches and works.
On vercel: something in the edge/proxy stack (rewrite, CDN, or your client) is hitting /api/users without the slash and the redirect either (a) gets stripped/looped or (b) never happens the same way. So only a route declared at "" matches reliably there.
The robust fix (works everywhere)
Turn off auto redirecting and
Serve both paths for every collection endpoint
\# main/index.py
from fastapi import FastAPI
app = FastAPI(title=settings.APP_TITLE, lifespan=lifespan, redirect_slashes=False)
# routes/users.py
from fastapi import APIRouter, Depends
from ..schemas.user import UserOut
from ..auth import require_admin
user_router = APIRouter(prefix="/users", tags=["users"])
@user_router.get("", include_in_schema=False, response_model=list[UserOut]) # /api/users
@user_router.get("/", response_model=list[UserOut]) # /api/users/
def get_users(_: UserOut = Depends(require_admin)):
return list_users()
This dual-decorator pattern for the users collection resolved above issue. This removes the ambiguity and avoids relying on redirects that may be handled differently by Vercel.