npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vettly/nextjs

v0.1.19

Published

Next.js middleware for content moderation. Content moderation for API routes.

Readme

@vettly/nextjs

Next.js integration for UGC moderation. Policy-governed decisions for App Router, Pages Router, and Middleware.

UGC Moderation Essentials

Apps with user-generated content need four things to stay compliant and keep users safe. This package handles all four in Next.js:

| Requirement | Next.js Integration | |-------------|---------------------| | Content filtering | moderateRoute(), moderateMiddleware() | | User reporting | Re-exported SDK client (POST /v1/reports) | | User blocking | Re-exported SDK client (POST /v1/blocks) | | Audit trail | result.decisionId on every decision |

// app/api/comments/route.ts
import { moderateRoute } from '@vettly/nextjs'
import { NextResponse } from 'next/server'

export const POST = moderateRoute({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'app-store',
  field: 'content',
  handler: async (req) => {
    const body = await req.json()
    await db.comments.create({ data: body })
    return NextResponse.json({ success: true })
  }
})

Why Next.js-Native Integration?

  • Edge-ready - Works in Edge Middleware and Edge Runtime
  • App Router support - Route Handlers with built-in moderation
  • Middleware protection - Protect multiple routes with a single config
  • Audit trail - Every decision linked to request for compliance

Installation

npm install @vettly/nextjs @vettly/sdk

Quick Start - Route Handler

Protect a single API route:

// app/api/comments/route.ts
import { moderateRoute } from '@vettly/nextjs'
import { NextResponse } from 'next/server'

export const POST = moderateRoute({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'community-safe',
  field: 'content',
  handler: async (req) => {
    const body = await req.json()

    // Content passed moderation
    await db.comments.create({ data: body })

    return NextResponse.json({ success: true })
  }
})

Quick Start - Middleware

Protect multiple routes at once:

// middleware.ts
import { moderateMiddleware } from '@vettly/nextjs'

export default moderateMiddleware({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'community-safe',
  field: async (req) => {
    const body = await req.json()
    return body.content
  }
})

export const config = {
  matcher: '/api/comments/:path*'
}

Middleware Options

import { moderateMiddleware } from '@vettly/nextjs'

export default moderateMiddleware({
  // Required
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'community-safe',
  field: async (req) => {
    const body = await req.json()
    return body.content
  },

  // Optional: Custom handlers for each action
  onBlock: (req, result) => {
    return NextResponse.json(
      {
        error: 'Content blocked',
        decisionId: result.decisionId,
        categories: result.categories.filter(c => c.triggered)
      },
      { status: 403 }
    )
  },

  onFlag: (req, result) => {
    // Log flagged content but allow through
    console.log(`Flagged: ${result.decisionId}`)
    // Return undefined to continue, or NextResponse to override
    return undefined
  },

  onWarn: (req, result) => {
    // Minor concern - add header but allow
    const response = NextResponse.next()
    response.headers.set('X-Content-Warning', 'true')
    return response
  }
})

Options Reference

| Option | Type | Required | Description | |--------|------|----------|-------------| | apiKey | string | Yes | Your Vettly API key | | policyId | string | No | Policy ID (default: 'moderate') | | field | string \| function | Yes | Field path or async extractor function | | onBlock | function | No | Custom response for blocked content | | onFlag | function | No | Custom handling for flagged content | | onWarn | function | No | Custom handling for warned content |


Route Handler Integration

For App Router API routes:

// app/api/posts/route.ts
import { moderateRoute } from '@vettly/nextjs'
import { NextResponse } from 'next/server'

export const POST = moderateRoute({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'social-media',
  field: 'content',

  handler: async (req) => {
    const body = await req.json()

    // Save with decision ID for audit trail
    // Note: moderationResult is available via closure in onBlock
    await db.posts.create({
      content: body.content,
      authorId: body.authorId
    })

    return NextResponse.json({ success: true })
  },

  onBlock: (req, result) => {
    return NextResponse.json(
      {
        error: 'Post content violates community guidelines',
        decisionId: result.decisionId,
        triggeredCategories: result.categories
          .filter(c => c.triggered)
          .map(c => c.category)
      },
      { status: 403 }
    )
  }
})

Dynamic Field Extraction

For complex request structures:

// Combine multiple fields
export const POST = moderateRoute({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'social-media',
  field: async (req) => {
    const body = await req.json()
    // Combine title and body for moderation
    return `${body.title}\n\n${body.body}`
  },
  handler: async (req) => {
    // ...
  }
})

Protecting Multiple Routes

Use middleware with route matchers:

// middleware.ts
import { moderateMiddleware } from '@vettly/nextjs'

export default moderateMiddleware({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'user-content',
  field: async (req) => {
    const body = await req.json()
    return body.content || body.text || body.message || ''
  }
})

export const config = {
  matcher: [
    '/api/comments/:path*',
    '/api/posts/:path*',
    '/api/reviews/:path*',
    '/api/messages/:path*'
  ]
}

Conditional Moderation

Skip moderation for certain requests:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { ModerationClient } from '@vettly/nextjs'

const client = new ModerationClient({ apiKey: process.env.VETTLY_API_KEY! })

export async function middleware(req: NextRequest) {
  // Skip GET requests
  if (req.method === 'GET') {
    return NextResponse.next()
  }

  // Skip trusted users (example: admin role in JWT)
  const token = req.cookies.get('auth-token')
  if (token && isAdmin(token.value)) {
    return NextResponse.next()
  }

  // Moderate other requests
  try {
    const body = await req.json()
    const result = await client.check({
      content: body.content,
      policyId: 'community-safe'
    })

    if (result.action === 'block') {
      return NextResponse.json(
        { error: 'Content blocked', decisionId: result.decisionId },
        { status: 403 }
      )
    }
  } catch (error) {
    // Fail open
    console.error('Moderation error:', error)
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/api/:path*'
}

Server Actions

Moderate in Server Actions:

// app/actions/post.ts
'use server'

import { ModerationClient } from '@vettly/nextjs'
import { revalidatePath } from 'next/cache'

const client = new ModerationClient({ apiKey: process.env.VETTLY_API_KEY! })

export async function createPost(formData: FormData) {
  const content = formData.get('content') as string

  // Check content
  const result = await client.check({
    content,
    policyId: 'community-safe'
  })

  if (result.action === 'block') {
    return {
      error: 'Content violates community guidelines',
      decisionId: result.decisionId
    }
  }

  // Save with audit trail
  await db.posts.create({
    content,
    moderationDecisionId: result.decisionId,
    moderationAction: result.action
  })

  revalidatePath('/posts')
  return { success: true }
}

Error Handling

The middleware fails open by default:

export default moderateMiddleware({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'community-safe',
  field: 'content',

  // On any error, request continues (fail open)
  // This is the default behavior
})

To fail closed, wrap in try/catch:

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { ModerationClient } from '@vettly/nextjs'

const client = new ModerationClient({ apiKey: process.env.VETTLY_API_KEY! })

export async function middleware(req: NextRequest) {
  try {
    const body = await req.json()
    const result = await client.check({
      content: body.content,
      policyId: 'community-safe'
    })

    if (result.action === 'block') {
      return NextResponse.json({ error: 'Blocked' }, { status: 403 })
    }

    return NextResponse.next()
  } catch (error) {
    // Fail closed - reject if moderation unavailable
    console.error('Moderation unavailable:', error)
    return NextResponse.json(
      { error: 'Content moderation unavailable' },
      { status: 503 }
    )
  }
}

Edge Runtime

The package is Edge-compatible:

// app/api/comments/route.ts
import { moderateRoute } from '@vettly/nextjs'

export const runtime = 'edge'

export const POST = moderateRoute({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'community-safe',
  field: 'content',
  handler: async (req) => {
    // Works in Edge Runtime
    const body = await req.json()
    // ...
  }
})

Re-exported SDK

The SDK client is re-exported for convenience:

import { ModerationClient, moderateRoute, moderateMiddleware } from '@vettly/nextjs'

// Use helpers for simple cases
export const POST = moderateRoute({ ... })

// Use client directly for complex flows
const client = new ModerationClient({ apiKey: '...' })
const result = await client.check({ content, policyId })

TypeScript Support

Full TypeScript support:

import { moderateRoute } from '@vettly/nextjs'
import type { NextRequest } from 'next/server'
import type { CheckResponse } from '@vettly/sdk'

export const POST = moderateRoute({
  apiKey: process.env.VETTLY_API_KEY!,
  policyId: 'community-safe',
  field: 'content',

  handler: async (req: NextRequest) => {
    // Typed request
  },

  onBlock: (req: NextRequest, result: CheckResponse) => {
    // Typed result with decisionId, action, categories, etc.
  }
})

Get Your API Key

  1. Sign up at vettly.dev
  2. Go to Dashboard > API Keys
  3. Create and copy your key

Links