Skip to content
AI Engineering

Wiring an Agentic Extraction Pipeline: Next.js 16, Prisma 7, and the AI SDK

A hands-on build of Firecrawl → generateObject + Zod → Prisma, on the verified-stable 2026 baseline: Next.js 16, Prisma 7, AI SDK 5, and Zod 4. Every version, price, and code pattern here is checked against its own docs, not memory.

Published · How this was researched

Back to Global Tech Search

Market Horizon

Now – 2027

Target Sector

Full-Stack Development, AI Engineering, Data Pipelines

Playwright, known pages

99%

DOM-driven automation reliability — vendor/community benchmark

Stagehand / AI-native

90%

Per-step reliability, community benchmark

Autonomous agents

77%

Full novel-task completion — vendor/community benchmark

This is the build companion to The Web Scraper Didn't Die — It Got a Protocol: the actual route handler that turns a URL into a typed database row, on the stack that's genuinely stable in mid-2026, not the one an LLM's training data remembers.

The stack, pinned to what's actually stable

Four packages, four confirmed-stable baselines: the Vercel AI SDK reached its version 5 general availability on July 31, 2025; Zod 4 went stable on May 19, 2025, shipped deliberately under the zod@3.25 subpath so the AI SDK ecosystem wouldn't break on the upgrade; Prisma 7.0.0 shipped November 19, 2025, making the Rust-free TypeScript client the default; Next.js 16 is current, with Turbopack on by default and middleware.ts renamed to proxy.ts. Write these as "5+", "4+", "7+", and "16+" in your own notes — newer minors and majors will exist by the time you read this, and the right move is checking the docs, not trusting a remembered number.

The pipeline itself is four steps: Firecrawl scrapes a URL to markdown, generateObject from the AI SDK converts a Zod schema to JSON Schema and calls the model's structured-output mode against that markdown, the SDK validates and auto-retries on malformed output, and Prisma writes the validated object to Postgres.

Wiring Firecrawl to a Zod schema with generateObject

Start with the schema, not the scraper — the shape you want out determines everything upstream. .describe() on each field is not decorative; it's the single highest-leverage lever for extraction accuracy, confirmed directly in the AI SDK's own GitHub discussion on this pattern (#1947) and in Vercel's own guidance. Mark genuinely optional fields .nullable() rather than letting the model guess:

// lib/firecrawl.ts
import Firecrawl from '@mendable/firecrawl-js'
 
export const firecrawl = new Firecrawl({ apiKey: process.env.FIRECRAWL_API_KEY! })
// app/api/extract/route.ts
import { NextResponse } from 'next/server'
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
import { firecrawl } from '@/lib/firecrawl'
import { prisma } from '@/lib/prisma'
 
export const maxDuration = 60
 
const ExtractionSchema = z.object({
  title: z.string().describe('The main title of the page'),
  summary: z.string().describe('A 2-3 sentence summary'),
  topics: z.array(z.string()).describe('Key topics covered'),
})
 
export async function POST(request: Request) {
  const { url } = await request.json()
  try {
    const scraped = await firecrawl.scrape(url, { formats: ['markdown'] })
    const { object } = await generateObject({
      model: openai('gpt-4o'),
      schema: ExtractionSchema,
      prompt: `Extract structured data from this page:\n\n${scraped.markdown}`,
    })
    const saved = await prisma.extraction.create({
      data: { url, title: object.title, summary: object.summary, data: object },
    })
    return NextResponse.json(saved)
  } catch (err) {
    return NextResponse.json({ error: (err as Error).message }, { status: 500 })
  }
}

Two details worth calling out because they're easy to get subtly wrong. First, Firecrawl's own docs show both import Firecrawl from '@mendable/firecrawl-js' and import { Firecrawl } from 'firecrawl' as valid — pick one and be consistent across a codebase rather than mixing them. Second, the AI SDK supports both a provider-specific call like openai('gpt-4o') and a unified model-string form like 'openai/gpt-5.2' depending on how you've configured providers — again, pick one convention and hold it. generateObject handles the retry logic on malformed structured output itself, per the AI SDK docs, so you don't need to hand-roll a retry loop around it.

The Route Handler, Visualized

Request → Firecrawl → generateObject → Prisma → Response

REQUEST{ url }FIRECRAWL.scrape() → markdowngenerateObjectZod schema validatesPRISMA.create() → Postgres200JSON

Persisting extractions with Prisma 7

Prisma 7 changes two things that matter for this exact pipeline: the generator block now points at prisma-client (not the old prisma-client-js) with an explicit output path, and the recommended pattern pairs it with a driver adapter rather than Prisma's older built-in query engine binary:

// prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model Extraction {
  id        String   @id @default(cuid())
  url       String
  title     String
  summary   String
  data      Json
  createdAt DateTime @default(now())
}

The client itself needs a singleton pattern in any serverless deployment — Next.js on Vercel included — because each hot function invocation that constructs a new PrismaClient without reuse will eventually exhaust your Postgres connection pool under concurrent load, a documented and common failure mode:

// lib/prisma.ts
import { PrismaClient } from '../src/generated/prisma'
import { PrismaPg } from '@prisma/adapter-pg'
 
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
 
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
export const prisma =
  globalForPrisma.prisma ?? new PrismaClient({ adapter })
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

If you're on Prisma Postgres specifically (Prisma's own serverless database, production-ready since its 6.4.0 release in February 2025), connection pooling is available via a ?pool=true flag or through Prisma Accelerate rather than requiring the adapter pattern above — worth checking which path applies to your database provider before copying this verbatim.

Mapping this to scout, reader, synthesizer

The companion piece to this one describes a three-layer pattern — a scout that discovers URLs, a reader that extracts them, a synthesizer that merges results — implemented in frameworks like CrewAI as three role-based agents. In raw AI SDK code, that same pattern collapses into three chained function calls inside one route handler: Scout is a Firecrawl /map call or a Brave Search API query that returns a list of candidate URLs; Reader is the firecrawl.scrape() call shown above, run once per URL; Synthesizer is a second generateObject call that takes the array of per-page objects and merges them against a rollup schema. Nothing here requires a separate orchestration framework unless you want one — for a handful of pages, three sequential awaits in one handler is the whole system.

Weekly npm Downloads

Snyk / npmtrends / Socket — live figures, re-check before citing

Live figures move weekly — re-check on npmjs.com/npmtrends before citing

Cost and timeout math before you ship

Three numbers decide whether this pipeline is cheap or expensive at scale, and all three are worth checking against their live pricing pages before you commit to a budget, not just trusting the figures below. Firecrawl's JSON extraction mode costs roughly 5 credits per page versus 1 credit for a plain scrape — at Standard-tier pricing (~$83 for 100,000 credits), that's the difference between roughly $0.83 and $4.15 per 1,000 pages before any LLM token cost is added on top. Claude Sonnet 4.5 runs $3 per million input tokens and $15 per million output tokens at platform.claude.com's published pricing; Sonnet 5, which launched June 30, 2026, carries an introductory $2/$10 rate through September 1, 2026, after which it moves to the same $3/$15 tier. And Vercel's function timeout is plan-gated: Hobby defaults to 10 seconds (configurable up to 60 via maxDuration), Pro defaults higher and can be configured up to 300 seconds — which matters directly for this pipeline, since a slow-rendering page plus a structured-output call can genuinely take longer than a Hobby-tier default allows.

None of these figures are exotic to find — they're one visit to firecrawl.dev/pricing, platform.claude.com/pricing, or vercel.com/docs away — but they're exactly the kind of number that goes stale silently in a tutorial and quietly blows a budget six months later. Check them before you deploy, not after the first invoice.

Sources

  • Vercel, "AI SDK — Generating Structured Data," ai-sdk.dev/docs
  • Vercel, AI SDK 5 general availability notes, July 31, 2025
  • Zod, "Introducing Zod 4," zod.dev/v4, stable May 19, 2025
  • Prisma, "Prisma 7.0.0" release notes, prisma.io/changelog, November 19, 2025
  • Prisma, "Announcing Prisma 6.19.0" (connection pooling), prisma.io/blog
  • Next.js, "Route Handlers" and "Upgrading: Version 16," nextjs.org/docs
  • Firecrawl, "Node SDK," docs.firecrawl.dev/sdks/node
  • Vercel, "Configuring Maximum Duration for Functions," vercel.com/docs
  • Anthropic, pricing documentation, platform.claude.com/docs/pricing

Written by Abhishek Kushwaha, founder and writer at Global Tech Search, based in Kathmandu, Nepal.

Claude API Pricing per 1M Tokens

platform.claude.com/pricing

Per platform.claude.com/pricing — Sonnet 5's intro rate steps up to $3/$15 on Sept 1, 2026

Firecrawl Credit Cost by Operation

firecrawl.dev/pricing

firecrawl.dev/pricing — JSON extraction costs 5x a plain scrape before any LLM token cost

Vercel Function Timeout by Plan

vercel.com/docs — configurable via maxDuration

vercel.com/docs — configurable via `export const maxDuration` in the route handler

Advantages

  • Every package version cited here (AI SDK 5, Zod 4, Prisma 7, Next.js 16) is pinned to its own dated release notes, not assumed from training data — a real risk in a stack that ships breaking majors roughly twice a year
  • The Prisma 7 driver-adapter pattern and the generateObject + Zod combination are both drawn directly from Vercel's and Prisma's own documentation, including the .describe()/.nullable() guidance that actually improves extraction accuracy
  • The singleton Prisma client pattern shown here specifically avoids the connection-pool exhaustion failure mode that hits naive serverless Prisma usage under concurrent requests

× Challenges

  • Package versions and npm download counts move monthly in this ecosystem — treat every number here as a checkpoint to re-verify at ai-sdk.dev, zod.dev, and prisma.io before you ship, not a permanent fact
  • LLM-based extraction costs real money per page on top of Firecrawl's own credit cost, and that compounds quickly at crawl scale — budget for both, not just one
  • This is a companion piece to a sourcing-and-infrastructure article, not a replacement for it — the legal and anti-bot risk discussion belongs there, not repeated here

Risk Assessment

A loose Zod schema is the single most common cause of this pipeline silently degrading — without .describe() hints and .nullable() on genuinely optional fields, the model drifts toward plausible-looking but wrong structured output, and generateObject's schema validation will happily pass a wrong-but-valid object through.

Abhishek Kushwaha

Written by Abhishek Kushwaha

Full-stack software engineer in Kathmandu, Nepal — six years shipping production Django and Next.js systems, most recently at Pinakin Technologies & Research Center. Writes Global Tech Search on what the AI buildout costs in power, water and silicon, measuring it first-hand where he can. More about the author →