@vettly/nextjs
v0.1.19
Published
Next.js middleware for content moderation. Content moderation for API routes.
Maintainers
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/sdkQuick 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
- Sign up at vettly.dev
- Go to Dashboard > API Keys
- Create and copy your key
Links
- vettly.dev - Sign up
- docs.vettly.dev - Documentation
- @vettly/sdk - Core SDK
- @vettly/react - React components
