Series Navigation: Part 1: Building the Agent | Part 2: Connect to Frontend (You are here)
In Part 1, we built a fully functional AI summarizer agent that can fetch content from URLs and generate intelligent summaries.
In this follow-up, you’ll learn how to connect that agent to a Next.js frontend using the Mastra Client, bringing your summarizer to life with a simple web interface.
Here’s what the final result looks like Ai-summarizer-agent.com:
Prerequisites
- Completed Part 1 of this series
- Basic knowledge of React and Next.js
- The AI Summarizer Agent from Part 1 (code available on GitHub)
Setting Up Next.js with Mastra
Let's start by setting up a Next.js project that integrates with our Mastra agent.
Step 1: Create a Next.js Project
If you're starting fresh, create a new Next.js app:
npx create-next-app@latest ai-summarizer-app
cd ai-summarizer-app
When prompted, select:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
-
src/directory: Yes - App Router: Yes
- Import alias: No (or default)
Step 2: Install Mastra Client
The Mastra Client allows your frontend to communicate with your agents:
npm install @mastra/core
Step 3: Copy Your Agent Code
Copy your agent code from Part 1:
- Copy the entire
src/mastrafolder from Part 1 - Place it in your new Next.js project at
src/mastra - Copy your
.envfile with the Gemini API key
Your project structure should now look like:
ai-summarizer-app/
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ └── summarize/
│ │ │ └── route.ts
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/
│ │ └── SummarizerUI.tsx
│ ├── mastra/
│ │ ├── agents/
│ │ │ └── summarizer.ts
│ │ ├── tools/
│ │ │ └── web-scraper.ts
│ │ └── index.ts
├── .env
├── package.json
└── tsconfig.json
Creating the API Route
Next.js API routes will act as the bridge between your frontend and the Mastra agent.
Step 1: Create the Summarize API Route
Create a new file at src/app/api/summarize/route.ts:
import { summarizerAgent } from '@/src/mastra/agents/summarizer';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { input } = await request.json();
if (!input || typeof input !== 'string') {
return NextResponse.json(
{ error: 'Input is required', success: false },
{ status: 400 }
);
}
const result = await summarizerAgent.generate(input, {
maxSteps: 5,
});
return NextResponse.json({
text: result.text,
success: true,
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Failed',
success: false,
},
{ status: 500 }
);
}
}
What's happening here:
- We accept a POST request with an
inputfield - We validate that input exists and is a string.
- We call the
summarizerAgentwith the input and an optional parameter called maxSteps (maxSteps defines how many “reasoning steps” the agent can take while generating the summary) - We return the summarized text as a JSON response for real-time updates.
- We handle errors gracefully
Part 3: Building the Frontend UI
Now let's create an interactive UI for our summarizer.
Step 1: Create the Summarizer Component
Create a new file at src/components/SummarizerUI.tsx:
'use client';
import { useState } from 'react';
type InputMode = 'url' | 'text';
export default function SummarizerUI() {
const [input, setInput] = useState('');
const [summary, setSummary] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [mode, setMode] = useState<InputMode>('url');
const handleSummarize = async () => {
if (!input.trim()) {
setError('Please enter a URL or text to summarize');
return;
}
setLoading(true);
setError('');
setSummary('');
try {
const response = await fetch('/api/summarize', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ input }),
});
if (!response.ok) {
throw new Error('Failed to generate summary');
}
const data = await response.json();
if (data.success) {
setSummary(data.text);
} else {
setError(data.error || 'Failed to generate summary');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong');
} finally {
setLoading(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey && mode === 'url') {
e.preventDefault();
handleSummarize();
}
};
return (
<div className="max-w-4xl mx-auto p-6 space-y-6">
{/* Header */}
<div className="text-center space-y-2">
<h1 className="text-4xl font-bold text-gray-900">AI Summarizer Agent</h1>
</div>
{/* Mode Selector */}
<div className="flex gap-2 justify-center">
<button
onClick={() => setMode('url')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === 'url'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
URL
</button>
<button
onClick={() => setMode('text')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
mode === 'text'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
Text
</button>
</div>
{/* Input Area */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
{mode === 'url' ? 'Enter URL' : 'Enter Text'}
</label>
{mode === 'url' ? (
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="https://example.com/article"
className="w-full px-4 py-3 border text-black border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={loading}
/>
) : (
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Paste your text here..."
rows={6}
className="w-full px-4 py-3 border text-black border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
disabled={loading}
/>
)}
</div>
{/* Submit Button */}
<button
onClick={handleSummarize}
disabled={loading || !input.trim()}
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Generating Summary...
</span>
) : (
'Summarize'
)}
</button>
{/* Error Message */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-800 text-sm">
<span className="font-medium">Error:</span> {error}
</p>
</div>
)}
{/* Summary Output */}
{summary && (
<div className="space-y-3">
<h2 className="text-xl font-semibold text-gray-900">Summary</h2>
<div className="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 border border-blue-200 rounded-lg">
<div
className="prose prose-sm max-w-none text-gray-800"
dangerouslySetInnerHTML={{
__html: summary.replace(
/\*\*(.*?)\*\*/g,
'<strong>$1</strong>'
),
}}
/>
</div>
</div>
)}
{summary && (
<button
onClick={() => {
setSummary('');
setInput('');
setError('');
}}
className="text-sm text-gray-600 hover:text-gray-900 underline"
>
Clear and start over
</button>
)}
</div>
);
}
What's happening here:
- We create a React component with state management for input, summary, loading, error, and mode (which toggles between 'url' and 'text' input types)
- We build a handleSummarize function that sends a POST request to
/api/summarizewith the user's input and updates the UI based on the response - We render a clean interface with mode selector buttons, dynamic input fields (text input for URLs, textarea for text), and a submit button with loading states
- We display the generated summary in a styled container with basic Markdown formatting (converting bold syntax to HTML)
- We handle errors gracefully by showing error messages in a red alert box and include a "Clear and start over" button to reset the interface
Step 2: Update the Home Page
Update src/app/page.tsx to use our new component:
import SummarizerUI from '@/components/SummarizerUI';
export default function Home() {
return (
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 py-12">
<SummarizerUI />
</main>
);
}
Step 3: Update the Layout (Optional)
Update src/app/layout.tsx to add metadata:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: 'AI Summarizer | Powered by Mastra',
description: 'Intelligent content summarization using AI agents',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
Testing Your Application
Let's test everything works correctly.
Run the Development Server
npm run dev
Open http://localhost:3000 in your browser.
Test URL Summarization
- Click the "URL" button
- Enter:
https://blog.google/technology/ai/google-gemini-ai/ - Click "Summarize"
- Watch the agent fetch and summarize the content
Test Text Summarization
- Click the " Text" button
- Paste a long article or text
- Click " Summarize"
- See the immediate summary generation
Test Error Handling
- Try an invalid URL
- Try empty input
- Verify error messages display correctly
Conclusion
You've successfully connected your Mastra AI agent to a beautiful Next.js frontend. You now have a production-ready application that:
What You've Learned
- Mastra Client Integration: How to connect frontend to an agents
- Next.js API Routes: Building backend endpoints for AI agents
- Modern UI/UX: Creating intuitive interfaces for AI applications
Complete Code Repository
The full working code for both Part 1 and Part 2 is available on GitHub:
Repository: github.com/yourusername/ai-summarizer-mastra
Additional Resources
Documentation
Happy building!
If you found this tutorial helpful, consider sharing it with other developers learning AI agent development!





Top comments (0)