This content originally appeared on DEV Community and was authored by Timmy Dahunsi
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!
This content originally appeared on DEV Community and was authored by Timmy Dahunsi
Timmy Dahunsi | Sciencx (2025-11-12T09:13:54+00:00) Build an AI Summarizer Agent in TypeScript Using Mastra (Part 2): Connect to Frontend. Retrieved from https://www.scien.cx/2025/11/12/build-an-ai-summarizer-agent-in-typescript-using-mastra-part-2-connect-to-frontend/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.




