





















































Hi ,
Welcome to WebDevPro #107!
This week we’re tackling one of the evergreen dev puzzles: how do you build something that’s smart, reliable, and delightful to use? In today's issue, we're looking at a project that answers that question. We'll be building an AI-powered email enhancer that turns rough drafts into polished, client-ready messages with a single click. We’ll stitch together React 19’s Server Functions and useActionState
for effortless form flows, bring in the Vercel AI SDK, and give it all a clean Tailwind finish. Think “practical patterns you can lift into any app,” not just a demo.
Much of the inspiration for this project comes from Carl Rippon’s Learn React with TypeScript (3rd Edition), which dives deep into React 19, TypeScript, and Next.js. It is a solid guide for developers who want to build maintainable, real-world web apps while staying ahead of the curve.
Back to this project, you'll learn:
By the end, you’ll see not only how to implement the feature technically, but also how these modern tools reshape development workflows.
Before diving in, here’s a quick look at last week’s top stories you may have missed:
Have any ideas you want to see in the next article? Hit Reply!
Advertise with us
Interested in reaching our audience? Reply to this email or write to kinnaric@packt.com.
Learn more about our sponsorship opportunities here.
Creating the foundation for the enhancer begins with a fresh Next.js project. Using the latest scaffolding options, you get TypeScript, Tailwind, ESLint, and Turbopack out of the box. Run the following commands in your terminal:
npx create-next-app@latest email-enhancer --typescript --tailwind --app --eslint --src-dir --turbopack --no-import-alias
cd email-enhancer
npm install ai zod @ai-sdk/openai
Once complete, open the email-enhancer folder in your preferred code editor. You now have a clean environment ready for server-side AI calls, validation, and styling, with all the dependencies in place to move forward.
Before calling any AI models, the application needs secure access to your OpenAI key. Create a .env.local file in the project root and add:
OPENAI_API_KEY=your_openai_api_key_here
This keeps credentials on the server side only, never exposed to the client, which is essential for protecting your account and keeping the bundle lightweight.
With the environment ready, the next step is to define the server logic that will handle email enhancement. Inside the src/actions folder, create a file named enhance-email.ts and add:
"use server";
import { openai } from "@ai-sdk/openai";
import { generateObject } from "ai";
import { z } from "zod";
const enhanceResultSchema = z.object({
originalTone: z.string(),
toneDescription: z.string(),
alternative: z.object({
optimizedContent: z.string(),
explanation: z.string(),
}),
});
type EnhanceEmailState = {
success: boolean;
errors?: string[];
result?: z.infer<typeof enhanceResultSchema>;
loading?: boolean;
content?: string;
};
A few key details stand out:
This approach sets the stage for a predictable, developer-friendly workflow, where AI output is both structured and trustworthy.
With the structure in place, the next step is to bring the enhanceEmail function to life. Every Server Function in React 19 accepts two parameters: the previous state and the incoming FormData. In this case, the state placeholder is named _ since it is not used directly.
export async function enhanceEmail(
_: EnhanceEmailState,
formData: FormData
): Promise<EnhanceEmailState> {
try {
} catch {
return {
success: false,
errors: [
"An error occurred while analyzing your email. Please try again.",
],
content: "",
};
}
}
The try-catch structure ensures that even unexpected failures return a clear error message rather than breaking the user experience.
Before any AI call is made, the submitted email draft must be checked for quality and length. This prevents unnecessary API usage while guiding users toward meaningful input.
const rawContent = formData.get("content");
const validationResult = z
.string()
.min(50, "Email content must be at least 50 characters long")
.max(2000, "Email content must be less than 2000 characters")
.refine(
(content) => content.trim().length > 0,
"Email content cannot be empty"
)
.safeParse(rawContent);
if (!validationResult.success) {
return {
success: false,
errors: validationResult.error.errors.map((err) => err.message),
content: typeof rawContent === "string" ? rawContent : "",
};
}
const content = validationResult.data;
The validation rules enforce a minimum of 50 characters to ensure substance and a maximum of 2000 characters to keep requests cost-efficient. If validation fails, the function immediately returns errors while preserving the draft content so nothing is lost.
Once validation succeeds, the function sends the draft to OpenAI through the Vercel AI SDK’s generateObject. By providing the schema upfront, the response is guaranteed to follow the required structure:
const { object } = await generateObject({
model: openai("gpt-4o-mini"),
schema: enhanceResultSchema,
prompt: `Analyze the tone of this email and provide alternative versions optimized for different contexts.
Original email: "${content}"
Please:
1. Identify the current tone (e.g., professional, casual, aggressive, friendly, etc.)
2. Provide a brief description of the current tone
3. Generate an enhanced version of the email that is professional, respectful, and client-focused
4. Explain what changes were made and why
Keep the core message and intent intact while adjusting the tone appropriately.
`,
});
return {
success: true,
result: object,
content: typeof rawContent === "string" ? rawContent : "",
};
This approach combines type safety with structured AI responses, ensuring the app never has to guess at the format of the returned data. Importantly, all calls remain server-side, meaning the OpenAI key defined in .env.local is never exposed to the client.
With the server logic in place, the next step is building the user-facing form. Start by creating components/enhance-email-form.tsx inside the src folder:
"use client";
import { useActionState } from "react";
import { enhanceEmail } from "@/actions/enhance-email";
import { EnhancedEmailDisplay } from "./enhanced-email-display";
export function EnhanceEmailForm() {}
Declaring "use client" ensures this component runs on the client side, which is required for React hooks. Here, the form will connect directly to the server function enhanceEmail, while also rendering a child component for displaying results, EnhancedEmailDisplay. The fact that a client component can call into a server function directly is part of what makes React 19 such a powerful upgrade for developer experience.
The useActionState hook ties the form directly to the server function, managing submission, pending states, and validation feedback without extra wiring.
export function EnhanceEmailForm() {
const [state, formAction, isPending] = useActionState(enhanceEmail, {
success: false,
});
}
Here’s the core structure of the enhancer form:
export function EnhanceEmailForm() {
const [state, formAction, isPending] = useActionState(...);
return (
<div className="max-w-4xl mx-auto p-6 space-y-8">
<h1 className="text-3xl text-center font-bold text-gray-900 mb-2">
Email Enhancer
</h1>
<form action={formAction} className="space-y-6">
<div>
<textarea
aria-label="Email content"
name="content"
rows={8}
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
state.errors ? "border-red-500" : "border-gray-300"
}`}
placeholder="Enter your draft email content here..."
disabled={isPending}
defaultValue={state.content ?? ""}
/>
{state.errors && (
<p className="mt-1 text-sm text-red-600">
{state.errors.join(". ")}
</p>
)}
</div>
<button
type="submit"
disabled={isPending}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isPending ? "Enhancing ..." : "Enhance Email"}
</button>
</form>
</div>
);
}
Key highlights:
When enhancement succeeds, the EnhancedEmailDisplay component renders the analysis and improved version of the email:
{state.success && state.result && (
<EnhancedEmailDisplay
originalTone={state.result.originalTone}
toneDescription={state.result.toneDescription}
alternative={state.result.alternative}
/>
)}
The display component highlights:
export function EnhancedEmailDisplay({
originalTone,
toneDescription,
alternative,
}: {
originalTone: string;
toneDescription: string;
alternative: {
optimizedContent: string;
explanation: string;
};
}) {
return (
<div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-blue-900 mb-2">
Current Tone Analysis
</h3>
<p className="text-blue-800 mb-2">
<span className="font-medium">Detected Tone:</span> {originalTone}
</p>
<p className="text-blue-700">{toneDescription}</p>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Optimized Version
</h3>
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
<div className="bg-gray-50 border rounded-md p-3 mb-4">
<p className="text-gray-800 whitespace-pre-wrap">
{alternative.optimizedContent}
</p>
</div>
<div>
<h5 className="text-sm font-medium text-gray-700 mb-2">
What Changed:
</h5>
<p className="text-gray-600 text-sm">{alternative.explanation}</p>
</div>
</div>
</div>
</div>
);
}
Finally, the main page imports and renders the form:
import { EnhanceEmailForm } from "@/components/enhance-email-form";
export default function Home() {
return (
<main className="min-h-screen bg-gray-50 py-8">
<EnhanceEmailForm />
</main>
);
}
Launching the app with npm run dev makes the enhancer available at http://localhost:3000. Each form submission sends a POST request to the server function, and responses are streamed back chunk by chunk, ensuring fast feedback for users.
✅ In action: a rough draft goes in, a professional email comes out. The enhancer not only transforms content but also teaches users how to refine tone through clear feedback on what was changed and why.
This project walked through the full cycle of building an AI-powered email enhancer with React 19, from configuring environment variables to deploying a user-friendly form. Along the way, we saw how Server Functions reduce the need for manual wiring, how Zod validation safeguards both user input and AI responses, and how the Vercel AI SDK creates type-safe integrations that feel seamless in practice. Tailwind CSS added polish without slowing development, keeping the focus on functionality.
The larger takeaway is that modern frameworks are not only about shipping features faster but about reshaping how we think about developer experience. By combining server-first form handling with reliable AI outputs, developers can deliver tools that feel both intelligent and trustworthy.
Think about your own workflow. Where could an AI-assisted enhancer help? Drafting code reviews, improving documentation, or polishing customer communication?
Pick one small scenario and prototype a form powered by a server function and validation. Even a simple experiment can surface new ideas for reducing friction in daily development.
Cheers!
Editor-in-chief,
Kinnari Chohan