Every server/api file is a public endpoint the moment you deploy
In Nuxt, a file at server/api/users.get.ts becomes a live route at /api/users as soon as the app is deployed. There is no gate in front of it. The route does not ask who is calling, it does not check a session, and it does not require a token unless you wrote code that does. That is the concrete launch risk: an endpoint your UI calls "for logged-in users" is, on the wire, reachable by anyone who knows or guesses the path.
This matters because Nuxt server routes are not the same as the page. Your page might hide a dashboard behind a login screen, but the data endpoint that fills the dashboard is a separate URL with its own access rules. If the only thing standing between an anonymous caller and the data is the fact that the link is not on a visible button, there is no real boundary at all.
Two things are worth separating here. What is observable is that the route exists and answers requests. What is only possible is that it returns sensitive data, and that depends entirely on what the handler does. An endpoint that returns a public list of blog posts is fine being open. An endpoint at /api/admin/users that returns email addresses to any caller is not. The Nuxt server routes security question is always: which of my open routes hand back data that should have required a logged-in user.
Why AI-built Nuxt apps hit this
AI scaffolding is very good at producing a working server route and very rarely good at protecting it. Ask a code tool to "make an endpoint that returns the current user's orders" and it will write a handler that queries the database and returns the rows. The handler works on the first request, the UI shows the data, and the feature looks done. The step that does not happen on its own is the line that reads the session and rejects an unauthenticated caller.
The generated handler usually trusts its inputs. A common pattern is reading an id from the query string, for example /api/orders?userId=42, and returning that user's records directly. Nothing in the scaffold ties userId to the caller's actual session, so changing the number returns someone else's data. That is an authorization gap, not a login bug: the request was never required to authenticate in the first place, so there is no identity to check the id against.
- The route returns data in the browser during the demo, so the missing auth check is invisible from the inside.
- AI tools optimize for "the feature works," and an open endpoint works perfectly for the developer who is already logged in.
- Fast deploy means the public URL exists before anyone lists which /api/* routes are reachable. This overlaps with the broader pattern in unauthenticated API endpoints in AI-built apps.
Check your own /api/* routes from the outside
You can confirm this on your own app without attacking anything. Start by listing your routes: look in the server/api and server/routes directories of your project. Every file there is a path. A file named server/api/profile.get.ts is the route /api/profile responding to GET.
Then test each one as an anonymous caller. The safe way is your own browser in a private window where you are not logged in, with DevTools open on the Network tab. Visit a page, watch which /api/* requests fire, then copy one of those request URLs and open it directly in the logged-out window. You can also send a plain request from the command line with curl and no cookies or auth header, against your own deployed URL only.
Pay attention to the response payload, not just the status code. A 200 that returns more fields than the UI shows, such as password hashes, internal flags, or other users' email addresses, is a leak even when the route is "for logged-in users."
- Suspicious result: a route that should require login returns real data, such as a JSON array of users, orders, or anything tied to an account, to a request with no session cookie and no token.
- Safer result: the same route returns a 401 or 403, or an empty result, for the unauthenticated caller. A 401 means the handler is checking auth and rejecting you, which is what you want.
- Repeat per route. Each server/api file has its own logic, so one protected endpoint tells you nothing about the next. Also try changing an id in the query string or path on an endpoint that did return your data, to see whether it will hand back a different user's record.
Add an auth check, and know what it does not cover
The fix lives inside your handlers and your server middleware. The reliable place to start is server middleware: a file in server/middleware runs on every server request, so it is where you read the session or token once and attach the caller's identity to the event. Nuxt server utilities such as getRequestHeader and the session helpers let you read the incoming credentials; reject the request early when they are missing for a protected route.
From there, two rules close the common gaps. First, require authentication on every route that returns account-scoped data, and return 401 when it is absent. Second, enforce authorization separately: derive the user id from the verified session, never from a query parameter the caller controls, and return only rows that belong to that user. Authentication answers "who are you," authorization answers "are you allowed to see this specific record," and an AI-scaffolded route often skips both.
- Centralize the "is this caller authenticated" check in server middleware so a new route is protected by default rather than by remembering to add a line.
- In each handler, read the user id from the session, then scope the database query to it. Treat any id arriving in the query string or body as untrusted.
- Keep secrets server-side. Use runtimeConfig, not publicRuntimeConfig, for API keys and database URLs, so they never ship to the client bundle. See Nuxt runtimeConfig client secret exposure for that specific failure.
An auth check in front of a route is not the whole job. It does not validate the contents of a request body, it does not stop a logged-in user from sending abusive or malformed input, and it does not protect against a route that simply returns too many fields. Input validation, rate limiting, and trimming the response shape are separate tasks from gating access.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check scans your public surface, including /api/* paths, and looks at what those endpoints return to an unauthenticated request. When a route hands back a response that looks like sensitive data, such as user records or account fields, to a caller with no auth, the scan flags that endpoint with severity and fix direction. It checks the thing an outside visitor can actually reach: the route, and the payload it returns without credentials.
What it does not do is read your handler code or map your whole route tree. It works from the outside, so it sees the endpoints it can find and reach, and it judges by the response, not by your intent. Deciding which routes should be public, and writing correct session and authorization logic, is work you do in your Nuxt server code and should pair with manual review of your auth model.
Next step: open your server/api directory, list every route, and confirm each one that returns account data rejects an unauthenticated request before it returns rows. Run a focused Launch Check against the public URL before sharing it, so an exposed endpoint shows up in your scan and not in someone else's request log.
A clean Launch Check does not prove every endpoint is correctly authorized. The scan sees what answers from outside without credentials; it cannot confirm that a route requiring login also scopes data to the right user, and it does not test business logic. Treat a flagged open endpoint as a launch blocker, and treat a clean result as one signal, not a guarantee.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.