惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
B
Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog
S
SegmentFault 最新的问题
腾讯CDC
博客园 - 叶小钗
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
Jina AI
Jina AI
A
About on SuperTechFans
博客园 - 司徒正美
C
Check Point Blog
博客园 - 聂微东
Microsoft Security Blog
Microsoft Security Blog
N
Netflix TechBlog - Medium
T
Tenable Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
小众软件
小众软件
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
K
Kaspersky official blog
Stack Overflow Blog
Stack Overflow Blog
Y
Y Combinator Blog
D
DataBreaches.Net
A
Arctic Wolf
I
InfoQ
量子位
IT之家
IT之家
Security Latest
Security Latest
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
G
Google Developers Blog
P
Proofpoint News Feed
P
Privacy International News Feed
T
Threatpost
L
Lohrmann on Cybersecurity
P
Proofpoint News Feed
G
GRAHAM CLULEY
V
Vulnerabilities – Threatpost
Martin Fowler
Martin Fowler
C
Cyber Attacks, Cyber Crime and Cyber Security
PCI Perspectives
PCI Perspectives
F
Full Disclosure

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Zod on the server and the client: the schema you define once and the three ways it breaks in runtime
Juan Torchia · 2026-06-13 · via DEV Community

Zod on the server and the client: the schema you define once and the three ways it breaks in runtime

80% of Next.js projects using Zod have the same schema imported from three different contexts. And only one of those three contexts behaves exactly the way Zod promises.

Yeah, you read that right. The mental model of "define the schema once and validate everywhere" is true inside the library — but in a real stack with Next.js 16 — Server Actions, edge middleware, and API routes — you're dealing with three execution environments with different constraints, and Zod doesn't always arrive complete to all three.

My thesis: Zod is one of the best tools in the TypeScript ecosystem, but sharing the same schema across client, Node.js server, and edge runtime without thinking about context differences produces three specific failure classes that aren't obvious until they blow up. This post documents those three failures and the pattern that prevents them.


The problem nobody draws in the diagram

When you start with Zod, the flow looks clean: a schema in lib/schemas/user.ts, imported from the Server Action, from the client form, and from the middleware. TypeScript happy, a single source of truth.

The problem is that .ts file runs in three different engines depending on context:

Context Runtime Relevant constraints
Client component / form Browser (V8) No Node APIs, bundle must stay small
Server Action / API Route Node.js on the server Full access, but strict serialization between server/client
Middleware (middleware.ts) Edge Runtime (restricted V8) No Node.js APIs, limited ESM modules, no eval

Zod itself is compatible with all these contexts at its core. The problem isn't Zod — it's what you build on top of Zod: refinements with Node.js logic, transforms that return non-serializable types, and errors that travel back to the client unfiltered.


Failure #1: The .refine() that silently calls Node.js

The first failure mode shows up in the middleware. You have a session or route parameter validation schema, you drop it in middleware.ts, and at some point that schema has a .refine() that does something innocent like this:

// lib/schemas/session.ts
import { z } from "zod"
import { isValidToken } from "@/lib/crypto" // ← uses Node.js crypto

export const sessionSchema = z.object({
  token: z.string().refine(
    async (val) => isValidToken(val), // ← calls a function with Node API
    { message: "Invalid token" }
  )
})

// middleware.ts — Edge Runtime
import { sessionSchema } from "@/lib/schemas/session"

export async function middleware(request: NextRequest) {
  const result = await sessionSchema.safeParseAsync({ token: getCookie(request) })
  // ← in Edge Runtime, this can fail if isValidToken uses Node's crypto.subtle
}

Next.js's edge runtime runs in a restricted V8 environment — similar to Cloudflare Workers — that doesn't expose all Node.js APIs. If isValidToken internally uses Node's crypto (not the Web Crypto API), the import explodes at runtime, not at build time. TypeScript won't catch it because the signature is valid.

The fix: schemas that go to the middleware have to be edge-safe by design. If you need validation logic that depends on Node.js APIs, that logic doesn't belong in the edge schema — it belongs in the Server Action running on Node.js.

// lib/schemas/edge/session.ts — structural validation only, no Node logic
import { z } from "zod"

export const edgeSessionSchema = z.object({
  token: z.string().min(32).max(512), // pure structural validation
  // No .refine() calling anything external
})

// lib/schemas/server/session.ts — for Server Actions / API Routes on Node.js
import { z } from "zod"
import { isValidToken } from "@/lib/crypto"

export const serverSessionSchema = z.object({
  token: z.string().refine(
    async (val) => isValidToken(val),
    { message: "Invalid token" }
  )
})

Separating schemas by execution layer isn't duplicating code — it's documenting the real contract for each context. You can read more about how the Web Crypto API differs between browser and Node.js in this stack analysis — the same logic applies to what you can safely put inside an edge .refine().


Failure #2: The .transform() that breaks serialization in Server Actions

The second failure mode is more subtle and only surfaces in Server Actions. When a Server Action returns data, Next.js serializes it to send to the client using a protocol based on React Server Components (similar to JSON but with support for Promises, Dates, and some special types). The official docs call these "serializable return values."

The problem: if your schema uses .transform() to convert data into something non-serializable — a Map, a class instance, a Set, or an object with methods — and that result travels directly to the client from a Server Action, Next.js can't serialize it.

// lib/schemas/user.ts — shared schema without thinking about context
import { z } from "zod"

export const userSchema = z.object({
  id: z.string(),
  roles: z.array(z.string()).transform(
    (roles) => new Set(roles) // ← Set is not serializable by React Server Components
  )
})

// app/actions/user.ts — Server Action
"use server"
import { userSchema } from "@/lib/schemas/user"

export async function getUser(formData: FormData) {
  const parsed = userSchema.parse({ id: formData.get("id"), roles: ["admin"] })
  return parsed // ← Next.js tries to serialize this → runtime error
}

TypeScript accepts this code. The build passes. The error shows up at runtime when Next.js tries to serialize the Set to send to the client component.

The most direct fix: if the transform exists for internal server convenience, don't put it in the shared schema. Put the base schema (without the transform) in the shared location, and apply the transform only inside the Server Action or service that needs it.

// lib/schemas/user.ts — base schema, no transforms that break serialization
import { z } from "zod"

export const userSchema = z.object({
  id: z.string(),
  roles: z.array(z.string()) // serializable array
})

// Clean inferred type for the client
export type User = z.infer<typeof userSchema>

// app/actions/user.ts
"use server"
import { userSchema } from "@/lib/schemas/user"

export async function getUser(formData: FormData) {
  const parsed = userSchema.parse({ id: formData.get("id"), roles: ["admin"] })
  // You do the Set transform here, on the server, and don't send it to the client
  const rolesSet = new Set(parsed.roles)
  return parsed // only the serializable object
}

This connects directly to the caching mental model in App Router: data traveling between server and client has constraints that TypeScript code doesn't reflect. If you want to go deeper on those constraints from the React side, the post on React 19 Server Components and caching covers the mental model that's missing from the documentation.


Failure #3: The Zod error that reaches the client unsanitized

The third failure is a security issue and it's the easiest one to introduce. When zod.parse() fails, it throws a ZodError with an array of issues. Each issue has path, message, and code. If you catch that error in a Server Action and send it directly to the client without processing it, you're shipping the complete internal validation structure — including internal field names, nested paths, and sometimes messages that leak business logic.

// ❌ Insecure pattern — the full ZodError travels to the client
"use server"
import { userSchema } from "@/lib/schemas/user"

export async function createUser(formData: FormData) {
  try {
    const data = userSchema.parse(Object.fromEntries(formData))
    // ...
  } catch (error) {
    // ← if it's a ZodError, this exposes internal paths to the client
    return { error: error instanceof Error ? error.message : "Unknown error" }
  }
}

ZodError.message is a serialized JSON with all the issues. On a password field or a field validating against an internal list of prohibited values, that can leak information.

The right pattern is to use safeParseAsync or safeParse and explicitly build the error message you want the client to receive:

// ✅ Secure pattern — sanitized errors
"use server"
import { userSchema } from "@/lib/schemas/user"

export async function createUser(formData: FormData) {
  const result = userSchema.safeParse(Object.fromEntries(formData))

  if (!result.success) {
    // You build exactly what you want to expose
    const publicErrors = result.error.issues.map((issue) => ({
      field: issue.path.join("."), // do you want to expose the path? your call
      message: issue.message,      // is this message safe for the client?
    }))
    return { success: false, errors: publicErrors }
  }

  // data is correctly typed
  const data = result.data
  // ...
  return { success: true }
}

This pattern also makes it much easier to internationalize error messages, because you have explicit control over what gets sent.


Decision checklist: how to share schemas across contexts

Before importing a schema from a new context, run it through these questions:

Will the schema run in Edge Runtime (middleware)?
  → Does it have .refine() or .transform() that calls external functions?
    → YES: separate into an edge-safe schema with structural validation only
    → NO: you can reuse it carefully

Will the schema be returned from a Server Action to the client?
  → Does it have .transform() that produces Map, Set, complex Date, class instance?
    → YES: apply the transform on the server, return the base serializable type
    → NO: the base schema can be shared

Will validation errors reach the client?
  → Are you using .parse() and catching the error directly?
    → YES: replace with .safeParse() and build the error response manually
    → NO: verify that error messages don't expose internal business logic

The golden rule: the shared schema should only have pure structural validations — types, lengths, formats, required fields. Validations that depend on business logic, database access, or Node.js APIs belong in server-exclusive schemas.


Limits: what you can't conclude without more evidence

This analysis is based on official Zod and Next.js documentation and reproducible patterns. What you can't assume from this:

  • That these three failure modes are the only ones. In a stack with tRPC, Remix, or Vercel Edge Functions, the constraints may differ.
  • That Next.js 16's edge runtime and Vercel's are identical in every case. The Next.js and Vercel Edge Runtime docs have some nuances of their own.
  • That .transform() always breaks serialization in Server Actions. Transforms that produce primitive types, arrays of primitives, or plain objects work fine. The problem is specific to types that aren't serializable by the React Server Components protocol.

If you want to verify the behavior in your own stack, the reproducible experiment is simple: create a schema with a .transform() that returns a new Set(), use it in a Server Action, and inspect the error in the browser console. Next.js's error message is pretty clear about what it can't serialize.


FAQ on Zod in production with Next.js 16

Can I use the same Zod schema on client and server?
Yes, if the schema only has pure structural validations (types, formats, lengths). The problem appears when you add .refine() with logic that depends on Node.js APIs or .transform() that produces non-serializable types.

Does Zod work in Next.js's Edge Runtime?
Zod's core does. Problems appear when .refine() or .transform() inside the schema call code that uses Node.js-exclusive APIs (like crypto, fs, or buffer). Zod itself doesn't use those APIs in its core.

What's the difference between parse() and safeParse() for Server Actions?
parse() throws a ZodError on failure, which you need to catch with try/catch. safeParse() returns { success: true, data } or { success: false, error } without throwing an exception. For Server Actions, safeParse() gives you explicit control over what errors you send to the client — which is the safe way to handle it.

Can I put database validations inside a Zod .refine()?
You can, but only in server schemas (never in schemas running on edge or client). An async .refine() that queries the database to check email uniqueness is a valid pattern in a Server Action on Node.js. In edge runtime or on the client, that makes no sense and isn't possible.

How do you know if a transform will break serialization in a Server Action?
The practical rule: if the resulting type of the transform is Map, Set, a class instance with methods, or anything that isn't serializable to plain JSON, don't return it directly from the Server Action. You can verify this in the official Next.js documentation on serialization in Server Actions.

Is it worth having separate schemas per context if it complicates the project?
The separation is only necessary where there are real differences: if you don't have middleware with validation logic, you don't need edge schemas. The minimum rule is: a shared base schema with structural validations, and extended schemas with .refine() / .transform() only where the context allows it.


Final stance and next step

Zod isn't broken. The "define once" model works perfectly for pure structural validations that don't depend on the execution environment. The problem is that in Next.js 16 with Server Actions and middleware, that execution environment changes silently and TypeScript doesn't warn you.

What I do buy: Zod as the source of truth for your types and data structure. What I don't buy without thinking: using the same schema with complex transforms and refinements across all three contexts without separating responsibilities.

The pattern that works is simple: a shared base schema with structural validations, server schemas for Node.js logic, and safeParse() whenever errors might travel to the client. It's not overhead — it's explicitly documenting what contract belongs to what layer.

The concrete next step: if you have a project with Zod in Next.js 16, find every place where you import a schema that has .refine() or .transform(), and verify what context it runs in. Three minutes of grep can save you a runtime error that only shows up in production.


Original sources:


This article was originally published on juanchi.dev