Scalable Nuxt.js Webhook Handling: The Strategy Pattern
Stop writing monolithic switch statements in your API routes. As your application grows, a single file handling subscription.created, payment.failed, and checkout.succeeded becomes unmaintainable.
Use Nuxt 4's server/utils directory. Functions exported here are auto-imported into your API routes, allowing you to separate the Service (logic) from the Controller (endpoint).
The Architecture
We split the logic into three parts:
- Handlers: Isolated logic for specific events.
- Dispatcher: A map that routes events to handlers.
- Endpoint: The API route that validates requests and triggers the dispatcher.
Directory Structure
server/
├── api/
│ └── webhooks/
│ └── polar.post.ts # 4. Entry Point
├── utils/
│ └── polar/
│ ├── index.ts # 3. Dispatcher (Service)
│ ├── types.ts # 1. Types
│ └── handlers/ # 2. Logic
│ ├── subscription.ts
│ └── checkout.ts
1. Define Types
Create a contract for your events to ensure type safety.
server/utils/polar/types.ts
export interface PolarEvent {
type: 'subscription.created' | 'subscription.updated' | 'checkout.created';
data: Record<string, any>;
id: string;
}
export type PolarHandler = (event: PolarEvent) => Promise<void>;
2. Create Handlers
Write isolated logic. These are pure functions that don't know about HTTP requests—only data.
server/utils/polar/handlers/subscription.ts
import type { PolarEvent } from '../types'
export const handleSubscriptionCreated = async (event: PolarEvent) => {
// Logic: Sync with DB, trigger welcome email
console.log(`Creating subscription: ${event.data.id}`)
}
export const handleSubscriptionUpdated = async (event: PolarEvent) => {
console.log(`Updating subscription: ${event.data.id}`)
}
3. Build the Dispatcher (Service)
This acts as the "brain". It maps event strings to functions. Because it's in server/utils, you can use polarService anywhere in your server.
server/utils/polar/index.ts
import { handleSubscriptionCreated, handleSubscriptionUpdated } from './handlers/subscription'
const handlers: Record<string, (e: any) => Promise<void>> = {
'subscription.created': handleSubscriptionCreated,
'subscription.updated': handleSubscriptionUpdated,
}
export const polarService = {
async handleEvent(event: any) {
const handler = handlers[event.type]
if (!handler) {
console.warn(`[Polar] Unhandled event: ${event.type}`)
return
}
await handler(event)
}
}
4. The API Endpoint
Keep this file thin. It handles the HTTP layer (validation, response) and delegates logic to the service.
server/api/webhooks/polar.post.ts
import { WebhookVerificationError } from 'polar-sdk' // Example SDK
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const headers = getRequestHeaders(event)
// 1. Security: Verify Signature
// if (!isValidSignature(body, headers['polar-signature'])) {
// throw createError({ status: 400, message: 'Invalid Signature' })
// }
// 2. Dispatch: Nuxt auto-imports 'polarService' from utils
try {
await polarService.handleEvent(body)
} catch (err) {
console.error('Webhook processing failed', err)
// Inform admin via email to investigate
// Log the error & context for further analysis
// Return 200 to acknowledge receipt even if processing fails
// to prevent retries loop (depending on provider policy)
}
return { received: true }
})
Why this works
- Isolation: If
subscription.tshas a syntax error, yourcheckoutlogic remains safe. - Scalability: To add a new event, you just create a file in
handlers/and add one line to the map inindex.ts. - Testing: You can write unit tests for
handleSubscriptionCreatedwithout mocking a full HTTP request context.
Making Nitro Scheduled Tasks Work Everywhere: A Complete Guide
Guide to implementing Nitro scheduled tasks across all deployment platforms including Vercel, Netlify, Cloudflare, Railway, and VPS setups.
10 Underrated Nuxt Modules You Need to Know About
Discover 10 powerful Nuxt modules that fly under the radar but can dramatically improve your development workflow, performance, and user experience.

