From harness-claude
Runs async Zod validation with parseAsync, safeParseAsync, async refinements, and external checks for DB uniqueness, service calls, and data transforms in server actions or API handlers.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Run async Zod validation with parseAsync, safeParseAsync, async refinements, and external checks
Uses Zod schemas for runtime validation of APIs, forms, env vars, and external data while inferring TypeScript types as single source of truth.
Defines type-safe Zod schemas for forms, APIs, env vars; covers parsing, refinements, transformations, type inference, and integrations with React Hook Form, Next.js, tRPC.
Share bugs, ideas, or general feedback.
Run async Zod validation with parseAsync, safeParseAsync, async refinements, and external checks
.parseAsync() when your schema contains async refinements or transforms:import { z } from 'zod';
import { db } from '@/lib/db';
const UniqueEmailSchema = z
.string()
.email()
.refine(
async (email) => {
const existing = await db.user.findUnique({ where: { email } });
return !existing;
},
{ message: 'Email address is already registered' }
);
// Must await — throws ZodError on failure
const validEmail = await UniqueEmailSchema.parseAsync(rawEmail);
.safeParseAsync() for error handling without try/catch:const result = await UniqueEmailSchema.safeParseAsync(rawEmail);
if (!result.success) {
const errors = result.error.flatten();
return { success: false, errors: errors.formErrors };
}
return { success: true, email: result.data };
const CreateAccountSchema = z
.object({
username: z.string().min(3).max(20),
email: z.string().email(),
password: z.string().min(8),
})
.superRefine(async (data, ctx) => {
const emailTaken = await db.user.findUnique({ where: { email: data.email } });
if (emailTaken) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Email is already in use',
path: ['email'],
});
}
const usernameTaken = await db.user.findUnique({ where: { username: data.username } });
if (usernameTaken) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Username is already taken',
path: ['username'],
});
}
});
// In a server action:
const result = await CreateAccountSchema.safeParseAsync(formData);
const UserIdSchema = z
.string()
.uuid()
.transform(async (id) => {
const user = await db.user.findUniqueOrThrow({ where: { id } });
return user; // Output type is User, not string
});
type ResolvedUser = z.infer<typeof UserIdSchema>; // User (database type)
const user = await UserIdSchema.parseAsync(rawId);
const SlugSchema = z
.string()
.min(1)
.regex(/^[a-z0-9-]+$/, 'Invalid slug format')
.refine(
async (slug) => {
// Only hit DB if synchronous checks pass
const existing = await db.post.findUnique({ where: { slug } });
return !existing;
},
{ message: 'Slug is already in use' }
);
Promise.all inside superRefine:const CreatePostSchema = z
.object({
slug: z.string().regex(/^[a-z0-9-]+$/),
categoryId: z.string().uuid(),
authorId: z.string().uuid(),
})
.superRefine(async (data, ctx) => {
const [slugExists, categoryExists, authorExists] = await Promise.all([
db.post.findUnique({ where: { slug: data.slug } }),
db.category.findUnique({ where: { id: data.categoryId } }),
db.user.findUnique({ where: { id: data.authorId } }),
]);
if (slugExists) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Slug already used', path: ['slug'] });
}
if (!categoryExists) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Category not found',
path: ['categoryId'],
});
}
if (!authorExists) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Author not found',
path: ['authorId'],
});
}
});
Sync schema with async context:
A schema without async refinements or transforms can still be used with .parseAsync() — it just resolves synchronously:
// This works fine (resolves synchronously under the hood)
const result = await z.string().email().safeParseAsync(rawEmail);
Error: "Schema must be used with parseAsync" — when it appears:
If a schema has an async refinement and you call .parse() (not .parseAsync()), Zod throws a synchronous error. This is a common mistake when extracting schemas from server-side code.
Timeout and abort:
Zod does not have built-in timeout support for async refinements. Wrap your async checks with a timeout utility if needed:
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Validation timeout')), ms)
)
return Promise.race([promise, timeout])
}
// Use inside refine:
.refine(async (email) => {
return withTimeout(checkEmailUniqueness(email), 3000)
})
Caching in async refinements:
Do not cache Zod schema instances that hold open DB connections or closures. Create them in the request scope or use a factory function.
https://zod.dev/api#parseAsync