Learn how to build a type-safe full stack architecture using TypeScript, Zod validation, and Next.js Server Actions with zero runtime mismatch.
Many web applications claim to be fully type-safe simply because both the frontend and backend are written in TypeScript. However, the network boundary represents a blind spot. When data moves from an HTML form across HTTP to a server, TypeScript types disappear. If an API endpoint casts incoming request bodies with 'as RequestPayload', you are trusting client input blindly.
Real end-to-end type safety requires two distinct mechanisms: static compile-time checking across codebases, and runtime validation at network boundaries. By pairing TypeScript with Zod and Next.js Server Actions, we can build a type-safe full stack architecture where data contracts are enforced strictly on both sides of the wire.
Rather than manually creating a TypeScript interface and a separate validation function, you define a single Zod schema. TypeScript then infers the static type directly from the runtime schema using 'z.infer'.
import { z } from "zod";
export const BookingSchema = z.object({
fullName: z.string().trim().min(3, "Name must be at least 3 characters").max(80),
email: z.string().trim().email("Please provide a valid email address"),
phone: z.string().trim().regex(/^\+?[1-9]\d{7,14}$/, "Invalid international phone number"),
visaType: z.enum(["tourist_30", "tourist_60", "transit"]),
passengersCount: z.coerce.number().int().min(1).max(10),
travelDate: z.string().refine((date) => !isNaN(Date.parse(date)), {
message: "Valid travel date required",
}),
});
export type BookingInput = z.infer<typeof BookingSchema>;This pattern ensures that changes to field names, validation constraints, or allowed enum values happen in exactly one place. If you add a field to the schema, TypeScript will immediately flag errors anywhere in your client forms or backend handlers where that field is omitted or mismatched.
In Next.js, Server Actions allow you to run server code directly without exposing conventional REST endpoints. Because Server Actions accept FormData or JSON objects, validating inputs through your schema is simple and safe.
"use server";
import { BookingSchema } from "@/lib/validations/booking";
export type ActionState = {
success: boolean;
message: string;
errors?: Record<string, string[]>;
};
export async function submitBookingAction(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const rawData = Object.fromEntries(formData.entries());
const validationResult = BookingSchema.safeParse(rawData);
if (!validationResult.success) {
return {
success: false,
message: "Validation failed. Please verify your entries.",
errors: validationResult.error.flatten().fieldErrors,
};
}
const cleanData = validationResult.data;
try {
// cleanData is fully typed as BookingInput
// Proceed to database write or payment gateway call
return {
success: true,
message: `Booking received for ${cleanData.fullName}. Processing request.`,
};
} catch (error) {
return {
success: false,
message: "Database operation failed. Please try again later.",
};
}
}
I implemented this exact end-to-end architecture on [Al Raheeq Tourism](/projects/17), a platform handling tourist visa requests and flight bookings. Travel applications require strict adherence to international passport standards and travel date validations. By sharing schemas between client validation and server actions, we eliminated form submission mismatches completely and prevented invalid submissions before touching downstream database tables.
With React 19 and modern Next.js, handling server responses on the client side no longer requires complex state machines or third-party query wrappers for forms. The 'useActionState' hook handles pending states, error payloads, and successful responses with full type checking.
"use client";
import { useActionState } from "react";
import { submitBookingAction, type ActionState } from "@/app/actions/booking";
const initialState: ActionState = {
success: false,
message: "",
};
export function BookingForm() {
const [state, formAction, isPending] = useActionState(submitBookingAction, initialState);
return (
<form action={formAction} className="space-y-4 max-w-md mx-auto">
<div>
<label htmlFor="fullName" className="block text-sm font-medium">Full Name</label>
<input id="fullName" name="fullName" required className="border rounded p-2 w-full" />
{state.errors?.fullName && (
<p className="text-red-500 text-xs mt-1">{state.errors.fullName[0]}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">Email</label>
<input id="email" name="email" type="email" required className="border rounded p-2 w-full" />
{state.errors?.email && (
<p className="text-red-500 text-xs mt-1">{state.errors.email[0]}</p>
)}
</div>
<button
type="submit"
disabled={isPending}
className="bg-blue-600 text-white py-2 px-4 rounded disabled:opacity-50"
>
{isPending ? "Submitting..." : "Book Application"}
</button>
{state.message && (
<p className={`text-sm ${state.success ? "text-green-600" : "text-red-600"}`}>
{state.message}
</p>
)}
</form>
);
}If your team is experiencing bugs caused by unsynchronized API types, unstable form handling, or insecure backend inputs, I can help audit your stack and implement clean type safety across your applications. Explore my [Full-Stack Web Development Services](/services) or [get in touch with Aafaque](/contact) to discuss your project.
TypeScript performs compile-time checks only and is completely erased during JavaScript execution. It cannot protect your server against corrupted payloads, malformed JSON, or malicious requests at runtime without a runtime validator like Zod.
Server Actions act as remote procedure calls (RPC). Instead of setting up API routes, writing manual fetch calls, and synchronizing request/response interfaces, you import and call the server function directly from your React component with full type inference.
Zod parsing overhead is negligible, typically running in fractions of a millisecond for typical business objects. The safety gains and prevention of broken database queries far outweigh the tiny CPU cost.