A 405 Method Not Allowed response means a server received your request but does not allow that HTTP method for the requested URL. In Next.js, a common cause is sending GET to a route that only handles POST, or making a fetch() request without setting the method you intended. This guide shows how to diagnose the mismatch, write a route that handles GET and POST, and test the response.
Key Takeaways
- In the App Router, a
route.tsfile handles a method only if it exports a matching function such asGETorPOST. fetch()usesGETby default. Setmethod: "POST"and provide a JSON body when sending a POST request.- A
405is different from a404: the path may exist, but the method is not supported there. - Check the actual request method, URL, and response status. A
fetch()promise does not reject just because the server returned405.
What a 405 Means in Next.js
HTTP methods describe what a client is asking a server to do. A browser address bar sends a GET request. A form or JavaScript client might send POST, PUT, or another method.
In the Next.js App Router, a Route Handler is defined in a route.js or route.ts file inside the app directory. It handles requests for the URL corresponding to that file. Export a function named for each method the route should accept. Supported method names include GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.
For example, app/api/items/route.ts handles requests to /api/items. If it exports only POST, a GET request to that path is a method mismatch. Next.js can respond with 405 Method Not Allowed when a method is not implemented by the Route Handler.
A 404 Not Found usually points to a URL that does not match a route. A 400 Bad Request usually means the server could not accept the request data. A 405 points you first toward the method.
Add GET and POST Handlers
Create app/api/items/route.ts:
export async function GET() {
return Response.json({ items: [{ id: 1, name: "Notebook" }] });
}
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json(
{ error: "Request body must be valid JSON" },
{ status: 400 }
);
}
if (
typeof body !== "object" ||
body === null ||
!("name" in body) ||
typeof body.name !== "string" ||
!body.name.trim()
) {
return Response.json(
{ error: "A non-empty name is required" },
{ status: 400 }
);
}
return Response.json(
{ item: { id: 2, name: body.name.trim() } },
{ status: 201 }
);
}
This example returns a sample item from GET. The POST handler validates JSON and returns a created-style response with status 201. It does not save data; connect it to your database or other storage if the route needs persistence. Invalid JSON and missing or blank names receive 400 rather than an unexplained server error.
Check the Request Method and URL
Compare the request your client sends with the method your route exports. These are common causes of a mismatch:
- You opened a POST-only endpoint in the address bar, which sends
GET. - Your client called
fetch(url)without options. That sendsGETby default. - A form is missing
method="post", or its method differs from the handler. - The request URL does not match the route file you edited.
- You are using the App Router but put the handler in the wrong directory, or you are using the Pages Router and expecting App Router behavior.
If you use the Pages Router, API routes are implemented differently: a default handler receives a request and response, and your code checks req.method and sets the response status. Do not copy an App Router export async function POST() handler into a Pages Router API file.
Also verify that the request reaches the Next.js server you are inspecting. A proxy, rewrite, middleware, deployment platform, or different running development server can return its own status. Check the response headers and relevant server logs if the route code does not appear to run.
Send POST Correctly with fetch()
Set the method explicitly, send a JSON body, and identify its content type:
async function createItem() {
const response = await fetch("/api/items", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Pen" }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error ?? `Request failed: ${response.status}`);
}
return data;
}
fetch() normally resolves to a Response for HTTP errors such as 400 or 405; it does not reject just because the status is outside the 2xx range. Check response.ok or response.status before treating the operation as successful. Network failures can reject the promise, so call this function from code that also handles exceptions. This example assumes the server returns JSON; if an endpoint can return an empty or non-JSON body, handle that response format before calling response.json().
A relative URL such as /api/items works in a browser page served from the same origin. For a request made in a server-side environment, configure the appropriate absolute origin when required by that environment.
Test the Route
Start your Next.js development server, then test both methods from a terminal. The -i option displays the response status and headers as well as the body.
curl -i http://localhost:3000/api/items
Expected: a successful status and a JSON body containing items.
curl -i -X POST http://localhost:3000/api/items \
-H 'Content-Type: application/json' \
--data '{"name":"Pen"}'
Expected: status 201 and a JSON body containing the created-style item.
To check the method-mismatch case, send a method the route does not export:
curl -i -X DELETE http://localhost:3000/api/items
Because the example exports GET and POST, it does not implement DELETE. The response should indicate that the method is not allowed. If the status differs, inspect the exact URL, the server handling the request, and any proxy or middleware in front of it.
Read the Response Status When Debugging
In browser developer tools, open the Network panel and inspect the failed request. Confirm the request URL and method, then read the status and response body. This often reveals that the browser sent GET when the code was intended to send POST, or that the request went to a different path.
You can also log the response while debugging:
const response = await fetch("/api/items", { method: "GET" });
console.log(response.status, response.statusText);
console.log(await response.text());
Use response.text() here to inspect the body as-is. A response body can only be consumed once, so do not read it as text and then try to parse the same body as JSON. For a quick look at a returned status code, the TidyURLs HTTP status checker may be useful; still test the actual request method and route behavior with your own client or curl.
Common Mistakes
- Assuming
fetch()sends POST automatically. It sendsGETunless you setmethod. - Testing a POST-only endpoint in the address bar. Address-bar navigation uses
GET. - Confusing the path with the method. A correct
/api/itemspath can still receive a405if the method is unsupported. - Ignoring the response status. A resolved
fetch()promise does not mean the HTTP request succeeded. - Sending an invalid body. For JSON, serialize the value with
JSON.stringify()and setContent-Type: application/json. - Mixing router conventions. App Router Route Handlers and Pages Router API routes use different handler patterns.
- Expecting a sample handler to persist data. The example returns a response but does not write to storage.
Best Practices
- Export only the methods your endpoint is designed to support, and document the expected request format for each.
- Validate request data on the server even if a browser form already checks it.
- Return meaningful status codes: for example,
400for invalid input and201when a resource is created. - Check
response.okorresponse.statusin clients, and handle network errors separately. - Test the route with the method and headers your real client will send. Include at least one unsupported-method test when checking a new endpoint.
Conclusion
A Next.js 405 is usually a sign that the request method and the route’s handlers do not match. Verify the URL, export the intended method, configure fetch() explicitly, and inspect the actual response. Testing with both the intended method and an unsupported one makes the mismatch easier to pinpoint.
References
- Next.js: Route Handlers — App Router Route Handler file conventions and supported HTTP methods.
