Article·  

Scalable Nuxt.js Webhook Handling: The Strategy Pattern

Learn how to implement scalable webhook handling in Nuxt.js using the Strategy Pattern to separate concerns and improve maintainability.

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:

  1. Handlers: Isolated logic for specific events.
  2. Dispatcher: A map that routes events to handlers.
  3. 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

  1. Isolation: If subscription.ts has a syntax error, your checkout logic remains safe.
  2. Scalability: To add a new event, you just create a file in handlers/ and add one line to the map in index.ts.
  3. Testing: You can write unit tests for handleSubscriptionCreated without mocking a full HTTP request context.

Copyright © 2026