A payment provider needs to send webhook callbacks into your Next.js App Router application. Would you implement that endpoint as a Server Action or as a route handler, and why?
answer
- the caller is not your own UI
- a third party needs a fixed path
- you must control the status code
- signature verification needs the raw body
- actions have build-generated ids
basics
~20 sA route handler. The provider needs a fixed URL, its own authentication, and a specific status code in reply; a Server Action has no published URL and is meant to be called only by your own application's UI.
solid answer
~40 sA route handler, because the caller is not my UI. The provider has to be given a path it can POST to for months, it authenticates with a signature rather than my session cookie, and it decides whether to retry based on the status code I return — so I need control of the URL, the raw body, the headers and the response. A Server Action gives me none of that: it is an RPC that my own components reference, and the endpoint behind it is generated by the build rather than documented, so there is nothing stable to hand a third party. The same reasoning applies to a native mobile client, a partner backend or a cron service: anything outside my own rendered app talks to a route handler.
code
typescript · 14 linesimport { verifySignature } from '@/lib/webhooks'
export async function POST(request: Request) {
const raw = await request.text()
const signature = request.headers.get('x-signature')
if (!verifySignature(raw, signature)) {
return new Response('Invalid signature', { status: 401 })
}
const event = JSON.parse(raw)
await recordEvent(event)
return new Response(null, { status: 204 })
}go deeper
Say plainly that outside callers need a route handler with a real URL, and that Server Actions are for your own UI. Knowing which file gives you a callable endpoint is the point here.
Explain the mechanics that force the choice: signature verification over the raw body, the status code driving the provider's retries, and the fact that an action's endpoint is generated rather than published.
Talk about operating it: verify, persist, return fast, process asynchronously, and make the handler idempotent because retries and duplicate deliveries are certain rather than hypothetical.
Frame it as contract ownership. Once a URL is published to a third party you owe it compatibility, so decide deliberately which endpoints become public, how they are versioned, and how their authentication is issued and rotated.
## The question behind the question Interviewers ask this to see whether you understand that a Server Action is not "a POST endpoint with nicer syntax". The two surfaces differ in who owns the contract. ## What a webhook sender actually requires A webhook provider is software you do not control and cannot redeploy. It needs four things from you: 1. **A stable path.** You paste a URL into their dashboard and it must keep working across your deploys. 2. **Its own authentication.** There is no browser and no session cookie. Providers sign the request, typically with an HMAC over the raw body in a header, and you verify that signature. 3. **Control of the response.** Providers key their retry behaviour off your status code: a `2xx` means accepted, a `5xx` means try again later, a `4xx` usually means give up. 4. **Access to the request as sent.** Signature verification runs over the exact bytes, so you need the raw body — `await request.text()` — not a re-serialised object. A route handler gives you all four. You export a `POST` function from a `route.ts` file, you read the request, and you return whatever `Response` you like. ## Why a Server Action cannot do this job A Server Action is an async function marked `'use server'`. It exists so your own React tree can call server code without you writing a URL or a `fetch`. The framework generates the endpoint and an id for it as part of the build; that id is an internal detail, not an interface you can publish, and nothing about it is a promise to outside callers. The invocation is also framework-shaped rather than provider-shaped: the provider would have to speak the framework's wire format, which it obviously does not. So the mismatch is not "actions are less powerful". It is that an action's contract belongs to the same deployment that calls it, and a webhook's contract belongs to someone else. ## Middleware is not the answer either A tempting shortcut is to catch the webhook path in `middleware.ts` since it sees requests early. Resist it. Middleware exists for cross-cutting decisions about requests headed elsewhere, and putting the handling of one specific path there means the check runs against traffic that has nothing to do with the provider, while your actual business logic ends up in the least testable, most latency-sensitive place in the application. ## The general rule to state out loud Draw the line at the edge of your own rendered app. Anything inside it — a form your components render, a button your client code wires up — can use a Server Action, because the caller ships in the same deployment. Anything outside it — webhooks, native apps, partner backends, schedulers, other services in your company — talks to a route handler, because you owe those callers a stable URL and a documented response. A useful follow-on for real systems: webhook handlers should do the smallest possible amount of work synchronously. Verify the signature, record the event, return quickly, and process asynchronously. Providers time out, and a slow handler turns into a storm of retries and duplicate deliveries — which is also why webhook handling should be idempotent.
- The same provider also wants a way for you to confirm an event was processed. Does that change your answer?No. That is an outbound call from your server to theirs, which is just an HTTP request from your own code — a route handler is still where the inbound side lives. What it does change is sequencing: record the event first, return quickly, and make the outbound confirmation part of the asynchronous processing so a slow third party cannot make the provider time out on you.
- Why should a webhook handler be idempotent?Because providers retry on timeouts and non-2xx responses, and networks duplicate deliveries, so the same event id will arrive more than once. Store the provider's event id and make reprocessing a no-op. Without that, one slow response turns into duplicate charges, duplicate emails, or double-applied state changes.
saying these in an interview costs you the question
- Server Actions are public POST endpoints a third party can call
- You can hand a provider the action id as a URL
- Handle the webhook in middleware since it sees requests first
- Verifying a signature over the parsed JSON is fine
- Any Next.js server code is reachable from outside by default