I Write about web, mobile development and modern JavaScript frameworks.
The best articles, links and news related to web and mobile development
No more broken JSON. No more parsing hacks. No more praying to the LLM gods. If you’ve ever built a chatbot, agent, or any AI-powered feature in Next.js, you know the pain: You beg the model: “Please respond with valid JSON only” It still wraps it in `json Your parser explodes half the time Production crashes at 3 AM There is now a 100% reliable solution in 2025, and it’s embarrassingly simple. It’s called .withStructuredOutput() in LangChain—paired with Zod TypeScript, you get perfectly typed, guaranteed JSON every single time. Here’s exactly how to do it, based on a full working Next.js 16 app using Gemini. This guide incorporates real code from a production-like implementation, including error handling, prompts for better control, and a sleek frontend with shadcn/ui. The One Line That Changes Everything `tsx const structured = model.withStructuredOutput(structureOutputSchema); ` That’s it. One line. Zero post-processing. LangChain natively forces models like Gemini, OpenAI, Groq, and Anthropic to obey your schema at the API level. Step-by-Step: Full Working Example (Next.js 16 TypeScript Gemini) Install the Bare Minimum Based on the project's package.json, here are the key dependencies: `bash npm install @langchain/google-genai zod For UI components (shadcn/ui via radix-ui, etc.): npm install @hookform/resolvers @radix-ui/react-label @radix-ui/react-progress @radix-ui/react-scroll-area @radix-ui/react-separator @radix-ui/react-slot class-variance-authority clsx lucide-react next-themes react-hook-form tailwind-merge Or for other providers: npm install @langchain/openai npm install @langchain/groq npm install @langchain/anthropic ` Define Your Bulletproof Schema (Zod = the Boss) Use Zod to define and validate your output structure. This ensures the model’s response matches exactly. `tsx // lib/zod-schema/structure-output.ts import { z } from "zod"; export const structureOutputSchema = z.object({ summary: z.string().min(1).max(1000), confidence: z.number().min(0).max(1), }); export type IStructureOutput = z.infer<typeof structureOutputSchema>; ` Set Up the Model `tsx // lib/models.ts import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; export const model = new ChatGoogleGenerativeAI({ model: "gemini-2.5-flash-lite", // Use the latest Gemini model for 2025 temperature: 0, // Deterministic for structured output apiKey: process.env.GEMINIAPIKEY, }); ` The Magic API Route (with Error Handling) This route handles requests, applies structured output, and includes prompts for guidance. It uses utility functions for standardized responses and error handling. `tsx // app/api/structure-output/route.ts import { model } from "@/lib/models"; import { structureOutputSchema } from "@/lib/zod-schema/structure-output"; import { formatResponse } from "@/lib/api-response"; import { routeErrorHandler } from "@/lib/api-error-handler"; export async function POST(request: Request) { try { const body = await request.json(); console.log(body); // Log for debugging (update from original) const system = "You are a concise assistant. Return only the requested JSON."; const user = Summarize for a beginner:\\n${body?.message}\\nReturn fields: summary (short paragraph), confidence (0..1); const structured = model.withStructuredOutput(structureOutputSchema); const response = await structured.invoke([ { role: "system", content: system }, { role: "human", content: user }, ]); return formatResponse(response, "Data fetched successfully"); } catch (error) { console.log("Error", { error }); return routeErrorHandler(error); } } ` Utility functions for responses: `tsx // lib/api-response.ts import { NextResponse } from "next/server"; type ApiResponse<T> = { success: boolean; message: string; data?: T; }; export function formatResponse<T>( data: T, message = "Operation completed successfully", status = 200, ) { return NextResponse.json<ApiResponse<T>>( { success: true, message, data }, { status }, ); } export function formatErrorResponse( message = "An error occurred", status = 500, ) { return NextResponse.json<ApiResponse<null>>( { success: false, message, data: null }, { status }, ); } ` Error handler: `tsx // lib/api-error-handler.ts import { ZodError } from "zod"; import { formatErrorResponse } from "./api-response"; export class HTTPError extends Error { statusCode: number; constructor(message: string, statusCode: number) { super(message); this.statusCode = statusCode; } } export function routeErrorHandler(error: unknown) { if (error instanceof ZodError) { const validationErrors = error.issues.map(issue => issue.message).join(", "); return formatErrorResponse(validationErrors, 422); } else if (error instanceof Error) { return formatErrorResponse(error.message, 500); } else { return formatErrorResponse("Internal server error. Please try again later", 500); } } ` Bonus: Clean Frontend with shadcn/ui (Full Chat Component) This frontend handles user input, displays messages with processing states, and renders structured outputs with confidence bars. `tsx // components/ChatPage.tsx "use client"; import { useState, useEffect, useRef } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { Send, Loader2, Bot, User, CheckCircle, Info } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { ScrollArea } from "@/components/ui/scroll-area"; const ChatSchema = z.object({ message: z.string().min(1, "Message cannot be empty"), }); type ChatSchemaType = z.infer<typeof ChatSchema>; type ChatMessage = { role: "user" | "bot"; text: string; confidence: number; summary: string; isProcessing?: boolean; }; export default function ChatPage() { const [messages, setMessages] = useState<ChatMessage[]>([]); const chatEndRef = useRef<HTMLDivElement>(null); const form = useForm<ChatSchemaType>({ resolver: zodResolver(ChatSchema), defaultValues: { message: "" }, }); useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); async function onSubmit(values: ChatSchemaType) { const userMessage = values.message; setMessages((prev) => [ ...prev, { role: "user", text: userMessage, confidence: 0, summary: "" }, { role: "bot", text: "Thinking...", confidence: 0, summary: "", isProcessing: true }, ]); form.reset(); form.setFocus("message"); try { const res = await fetch("/api/structure-output", { method: "POST", body: JSON.stringify({ message: userMessage }), headers: { "Content-Type": "application/json" }, }).then((r) => r.json()); setMessages((prev) => { const newMessages = [...prev]; const processingIndex = newMessages.findIndex((m) => m.isProcessing); if (processingIndex !== -1) { if (res.success) { newMessages[processingIndex] = { role: "bot", text: "", confidence: res.data.confidence, summary: res.data.summary, isProcessing: false, }; } else { newMessages[processingIndex] = { role: "bot", text: res.message || "An error occurred during processing.", confidence: 0, summary: "", isProcessing: false, }; } } return newMessages; }); } catch (error) { setMessages((prev) => { const newMessages = [...prev]; const processingIndex = newMessages.findIndex((m) => m.isProcessing); if (processingIndex !== -1) { newMessages[processingIndex] = { role: "bot", text: "Network error. Failed to connect to the backend.", confidence: 0, summary: "", isProcessing: false, }; } return newMessages; }); } } return ( <div className="max-w-3xl mx-auto py-8 px-4 h-screen flex flex-col bg-background"> <h1 className="text-3xl font-extrabold mb-4 flex items-center gap-2 text-gray-800 dark:text-gray-200"> <Bot className="h-7 w-7 text-primary" /> Structured Chat Demo </h1> <Separator className="mb-6" /> <ScrollArea className="flex-1 h-[calc(100vh-200px)] mb-6 p-4 border rounded-xl shadow-lg bg-card/50"> <div className="space-y-6"> {messages.length === 0 ? ( <div className="text-center pt-20"> <Info className="h-10 w-10 mx-auto text-muted-foreground mb-3" /> <p className="text-lg text-muted-foreground font-semibold">Ready for your query.</p> <p className="text-sm text-muted-foreground mt-1">Ask a question to see the confidence and summary structure.</p> </div> ) : ( messages.map((m, i) => ( <div key={i} className={flex ${m.role === "user" ? "justify-end" : "justify-start"} animate-in fade-in duration-300} > <div className={`max-w-[90%] md:max-w-[75%] flex items-start gap-3 p-3 rounded-2xl transition-all duration-300 ${ m.role === "user" ? "bg-primary text-primary-foreground rounded-br-none shadow-md" : "bg-secondary text-secondary-foreground rounded-bl-none border border-gray-200 dark:border-gray-700 shadow-sm" }`} > <div className={`w-7 h-7 flex-shrink-0 flex items-center justify-center rounded-full text-white font-bold text-sm ${ m.role === "user" ? "bg-primary-foreground text-primary" : "bg-primary" }`} > {m.role === "user" ? <User size={16} /> : <Bot size={16} />} </div> <div className="flex-1 min-w-0"> {m.isProcessing && ( <div className="flex items-center space-x-2 text-sm text-muted-foreground animate-pulse font-medium"> <Loader2 className="h-4 w-4 animate-spin text-primary" /> <span>AI is processing the request...</span> </div> )} {m.text && !m.isProcessing && ( <p className="whitespace-pre-line text-sm md:text-base break-words">{m.text}</p> )} {m.role === "bot" && m.confidence > 0 && ( <Card className="mt-2 bg-background/80 border-2 border-primary/20 shadow-lg"> <CardContent className="p-4 space-y-3"> <div className="flex items-center gap-2 text-sm text-primary font-bold border-b pb-2"> <CheckCircle className="h-4 w-4" /> Structured Confidence Result </div> <div className="space-y-1"> <p className="text-xs font-semibold text-muted-foreground flex justify-between"> <span>Confidence Score</span> <span>{(m.confidence 100).toFixed(1)}%</span> </p> <Progress value={m.confidence 100} className={`h-2 w-full transition-all duration-500 ${ m.confidence > 0.8 ? "[&>]:bg-green-500" : m.confidence > 0.5 ? "[&>]:bg-yellow-500" : "[&>]:bg-red-500" }`} /> </div> <Separator /> <div className="space-y-1"> <p className="text-xs font-semibold text-muted-foreground">Summary:</p> <p className="text-sm italic text-gray-700 dark:text-gray-300 bg-secondary/30 p-2 rounded"> {m.summary} </p> </div> </CardContent> </Card> )} </div> </div> </div> )) )} <div ref={chatEndRef} /> </div> </ScrollArea> <div className="mt-auto pt-4 bg-background sticky bottom-0"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="flex gap-3"> <FormField control={form.control} name="message" render={({ field }) => ( <FormItem className="flex-1 relative"> <FormControl> <Input placeholder="Ask the AI a question to get a structured response..." {...field} disabled={form.formState.isSubmitting} className="h-14 text-base rounded-xl border-2 pr-14 focus-visible:ring-primary" onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if ((field?.value ?? "").trim() && !form.formState.isSubmitting) { form.handleSubmit(onSubmit)(); } } }} /> </FormControl> <FormMessage className="absolute -bottom-6 left-0" /> </FormItem> )} /> <Button type="submit" disabled={form.formState.isSubmitting || !form.getValues("message")} className="h-14 px-6 rounded-xl shadow-lg transition-all duration-200 hover:scale-[1.02]" > {form.formState.isSubmitting ? ( <Loader2 className="h-5 w-5 animate-spin" /> ) : ( <Send className="h-5 w-5" /> )} <span className="sr-only md:not-sr-only md:ml-2">Send</span> </Button> </form> </Form> </div> </div> ); } ` That’s it. You now have guaranteed, typed JSON responses with a full chat interface. Why .withStructuredOutput() Wins in 2025 | Method | Reliability | TypeScript Safety | Speed | Developer Happiness | | --| --| --| --| --| | “Please return JSON” | ~60% | ❌ | Medium | 😭 | | Output parsers regex | ~85% | ⚠️ | Slow | 😤 | | .withStructuredOutput() | 99.9% | ✅ Full | Fastest | 🥹 | This method leverages model-native enforcement (e.g., JSON mode or function calling), reducing tokens and costs. Community discussions in 2025 highlight its robustness for production apps, with fixes for edge cases like schema mismatches. Works With Every Major Provider (Just Swap One Line) `tsx // OpenAI import { ChatOpenAI } from "@langchain/openai"; const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }); // Groq (blazing fast cheap) import { ChatGroq } from "@langchain/groq"; const model = new ChatGroq({ model: "llama-3.1-70b-versatile" }); // Anthropic import { ChatAnthropic } from "@langchain/anthropic"; const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" }); ` The same .withStructuredOutput(schema) line works across all. Frequently Asked Questions Q: Is .withStructuredOutput() available in LangChain JS/TS? A: Yes—fully supported since early 2024 and rock-solid in 2025, with ongoing improvements for providers like Gemini. Q: Do I still need to say “return JSON” in the prompt? A: Not required, but including a system prompt like "Return only the requested JSON" can enhance consistency. The API enforces the schema regardless. Q: Can I use arrays, nested objects, enums, etc.? A: Absolutely. Zod handles complex schemas: `tsx z.object({ steps: z.array(z.string()), categories: z.array(z.enum(["tech", "design", "business"])), metadata: z.object({ source: z.string(), author: z.string().optional(), }), }) ` Q: Is it slower or more expensive? A: Actually faster and cheaper—fewer tokens, no extra text. Gemini's structured mode optimizes this further. Get Started in 5 Minutes Grab a free Gemini API key → https://aistudio.google.com/app/apikey Add it to .env.local Copy the code above npm run dev Test with a query like "Explain quantum computing" You now have the most reliable AI backend possible in 2025. No more JSON.parse() crashes. Just perfect, predictable, typed responses—every time. Structured output is life. Save this post. Bookmark it. You’ll thank yourself later. Happy coding! 🚀 Resources LangChain Google GenAI Integration LangChain OpenAI Integration LangChain Groq Integration Google Gemini Models LangChain Structured Output Guide StructuredOutputMethodOptions Reference StructuredOutputType Reference Key Citations: LangChain JS Structured Output Guide LangChain Forum: Best Practices for Structured JSON GitHub Issue: Error Handling in withStructuredOutput Medium: Beginner's Guide to Structured Outputs Robin Wieruch: LangChain JS Structured Output Hashnode: Structured Output with Ollama and LangChainJS
Introduction Every developer starting with LLMs usually begins the same way: You call OpenAI, Gemini, or Groq’s API directly — send a prompt, get a response, and return “Hello world.” But as soon as your application grows into something stateful, multi-provider, retrieval-based, or agent-driven, raw API code becomes messy, repetitive, and difficult to scale. That’s where LangChain solves the problem: A unified, flexible, scalable framework that lets you build advanced AI systems without drowning in boilerplate. In this article, you’ll see: Why raw LLM APIs become painful at scale Your complete API-based “Hello world” setup The same calls implemented with LangChain How LangChain simplifies multi-provider LLM development When you should still use direct APIs Let’s start with the reality of raw LLM integration. The Problem with Raw LLM APIs (Short, Clear Version) Working directly with OpenAI, Gemini, and Groq APIs works fine for small demos, but becomes a real challenge when you build larger, multi-provider applications. Each provider has its own quirks, meaning you spend more time managing API differences than building features. Here’s why: Different Request Formats Every provider expects a different payload structure. OpenAI/Groq use messages[], while Gemini uses contents[].parts[]. Switching providers means rewriting your entire request body. Different Response Structures OpenAI returns choices[0].message.content, Gemini returns candidates[0].content.parts[0].text. This forces you to write separate parsing logic for each provider. Multiple Type Definitions You must create separate TypeScript interfaces for each LLM’s request/response shape. As APIs evolve, you constantly update types and keep them in sync. Repeated Boilerplate Code HTTP headers, tokens, roles, fetch calls, JSON parsing, and error handling— you copy-paste the same logic across every provider function. Vendor-Specific Behavior Different role systems, safety settings, error formats, streaming methods, and rate limits require custom handling per provider. Hard Manual Switching Changing models requires updating: payload structure, response parsing, types, model names, URLs, and error logic. High Maintenance Over Time As soon as you add more providers, more models, or more features, the codebase gets harder to maintain, test, scale, and refactor. Raw API Code (OpenAI, Gemini, Groq) Below is your existing implementation that shows how developers traditionally integrate multiple LLMs without LangChain. This raw approach requires handling different request formats, response structures, type definitions, and model-specific behavior manually. Types & Enums Important Note About OpenAI Roles: Before the o1 models, OpenAI uses role = "system" After the o1 models, OpenAI requires role = "developer" This comes from OpenAI’s chat completion specification. `tsx enum OpenAiRole { system = "system", user = "user", assistant = "assistant", developer = "developer", } export type IHelloOutput = { ok: true; provider: "openai" | "gemini" | "groq"; model: string; message: string prompttokens?: number; completiontokens?: number; totaltokens?: number; } // Gemini request/response types follow Google’s GenerateContent API spec export type IGeminiGenerateContent = { candidates?: { content?: { parts?: { text?: string }[] } }[] usageMetadata: { promptTokenCount: number; candidatesTokenCount: number; totalTokenCount: number; } } export type IGeminiRequestContent = { contents?: { parts?: { text?: string }[] }[] } // OpenAI/Groq follow the OpenAI Chat Completions response format export type IOpenAiGenerateContent = { choices: { message: { role: OpenAiRole; content: string; } }[] usage?: { prompttokens: number; completiontokens: number; totaltokens: number; } } export type IOpenAiRequestContent = { model: string; messages: { role: OpenAiRole; content: string; }[] temperature?: number; } ` Hello Gemini (uses Google AI’s GenerateContent API spec) Gemini’s body structure follows the Google AI Studio GenerateContent endpoint, which uses contents[].parts[].text instead of OpenAI’s messages[]. `tsx async function helloGemini(): Promise<IHelloOutput> { if (process.env.GEMINIAPIKEY === "None") { throw new Error("GEMINIAPIKEY is not set"); } const model = "gemini-2.0-flash-lite"; const url = https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${process.env.GEMINIAPIKEY}; const requestBody: IGeminiRequestContent = { contents: [{ parts: [{ text: "Hello world" }], }], }; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(Gemini API request failed: ${response.statusText} : ${await response.text()}); } const responseData: IGeminiGenerateContent = await response.json(); const message = responseData.candidates?.[0]?.content?.parts?.[0]?.text || "No message returned"; return { ok: true, provider: "gemini", model, message, prompttokens: responseData.usageMetadata.promptTokenCount, completiontokens: responseData.usageMetadata.candidatesTokenCount, totaltokens: responseData.usageMetadata.totalTokenCount, }; } ` Hello Groq (OpenAI-Compatible API Schema) Groq intentionally mirrors the OpenAI Chat Completions format. Groq doucmentation: https://console.groq.com/docs/openai#configuring-openai-to-use-groq-api Its documentation explicitly states that you should use the same structure as OpenAI — the only difference is the base URL. `tsx async function helloGroq(): Promise<IHelloOutput> { if (process.env.GROQAPIKEY === "None") { throw new Error("GROQAPIKEY is not set"); } const url = https://api.groq.com/openai/v1/chat/completions; const requestBody: IOpenAiRequestContent = { model: "llama-3.1-8b-instant", messages: [ { role: OpenAiRole.developer, content: "You are a helpful assistant." }, { role: OpenAiRole.user, content: "Hello world" } ], temperature: 0.7, }; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": Bearer ${process.env.GROQAPIKEY}, }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(Groq API request failed: ${response.statusText} : ${await response.text()}); } const responseData: IOpenAiGenerateContent = await response.json(); const message = responseData.choices?.[0]?.message?.content || "No message returned"; return { ok: true, provider: "groq", model: requestBody.model, message, prompttokens: responseData.usage?.prompttokens, completiontokens: responseData.usage?.completiontokens, totaltokens: responseData.usage?.totaltokens, }; } ` Hello OpenAI (Classic Chat Completions API) Uses the standard request/response defined in the OpenAI Chat Completions documentation. `tsx async function helloOpenAi(): Promise<IHelloOutput> { if (process.env.OPENAIAPIKEY === "None") { throw new Error("OPENAIAPIKEY is not set"); } const url = https://api.openai.com/v1/chat/completions; const requestBody: IOpenAiRequestContent = { model: "llama-3.1-8b-instant", messages: [ { role: OpenAiRole.developer, content: "You are a helpful assistant." }, { role: OpenAiRole.user, content: "Hello world" } ], temperature: 0.7, }; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": Bearer ${process.env.OPENAIAPIKEY}, }, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(OpenAI API request failed: ${response.statusText} : ${await response.text()}); } const responseData: IOpenAiGenerateContent = await response.json(); const message = responseData.choices?.[0]?.message?.content || "No message returned"; return { ok: true, provider: "openai", model: requestBody.model, message, }; } ` Express Server (Provider Router) To get same structured Basic nodeJS server with typescript visit: Building a TypeScript Express API for Vercel Deployment: A Step-by-Step Guide `tsx import express, { Request, Response, Application } from "express"; import bodyParser from "body-parser"; import dotenv from "dotenv"; import cors from "cors"; import { helloProvider } from "./providers"; dotenv.config(); const PORT = process.env.PORT || 4000; const app: Application = express(); app.use(bodyParser.json()); app.use(cors()); app.get("/", (req: Request, res: Response) => { res.send("Assalamualikum"); }); app.get("/hello/:provider", async (req: Request, res: Response) => { const provider = req.params.provider; switch (provider) { case "openai": return res.json(await helloProvider.openai()); case "gemini": return res.json(await helloProvider.gemini()); case "groq": return res.json(await helloProvider.groq()); default: return res.status(400).json({ ok: false, error: "Unsupported provider" }); } }); app.listen(PORT, () => console.log(Server running on port ${PORT})); ` Why This Raw Approach Becomes a Problem Using direct APIs works for simple demos, but as soon as your application grows—even slightly—you begin to feel the pain of maintaining three completely different LLM providers. The more features you add, the more your code turns into duplicated logic, scattered integrations, and provider-specific exceptions. Here’s what developers almost immediately run into: Repeated Boilerplate Everywhere Each provider requires its own fetch calls, headers, error checks, and JSON parsing. Most of your code ends up doing the same thing three different ways. Different Request & Response Shapes OpenAI/Groq use messages[], Gemini uses contents[].parts[]. OpenAI returns choices[].message.content, Gemini returns candidates[].content.parts[].text. You end up writing custom parsing logic for every provider. Hard-Coded Model and Provider Logic Every time you switch a model—or want to test multiple providers—you must update: URLs, request bodies, types, parsing logic, token extraction mappings, and role behavior. Manual Token & Role Management Different token fields, different system/developer role rules, different usage objects. You must track all of these separately. Scaling Becomes Hard As soon as you add new features like RAG, embeddings, memory, or agents, your raw API code starts to grow uncontrollably. You end up reinventing entire frameworks just to keep your app organized. In short: Raw API integration doesn’t scale. It only gets more complex the more providers and features you add. This is exactly why LangChain exists—to unify providers, remove boilerplate, simplify workflows, and let you focus on building features instead of maintaining low-level API details. The LangChain Version (Simpler, Cleaner, Unified) With LangChain, the exact same “Hello world” logic becomes dramatically simpler. No custom fetch calls. No manual headers. No API-specific request bodies. No provider-specific parsing logic. No duplicated types. LangChain provides a unified chat model interface, so every provider—OpenAI, Gemini, Groq—behaves the same from your code’s perspective. This removes 80–90% of the boilerplate you wrote earlier. Below is the equivalent implementation using LangChain. Hello Gemini with LangChain `tsx import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; const model = new ChatGoogleGenerativeAI({ model: "gemini-2.0-flash-lite", apiKey: process.env.GEMINIAPIKEY, }); const response = await model.invoke("Hello world"); console.log(response.text); ` ✔ No nested contents[].parts[] ✔ No endpoint URLs ✔ No manual parsing of candidates[].content.parts[] ✔ Same interface as OpenAI/Groq Hello OpenAI with LangChain `tsx import { ChatOpenAI } from "@langchain/openai"; const model = new ChatOpenAI({ model: "gpt-4o-mini", apiKey: process.env.OPENAIAPIKEY, }); const response = await model.invoke("Hello world"); console.log(response.text); ` ✔ No manual messages[] array ✔ No system/developer role management ✔ No response drilling into choices[0].message.content Hello Groq with LangChain `tsx import { ChatOpenAI } from "@langchain/openai"; const model = new ChatOpenAI({ model: "llama-3.1-8b-instant", apiKey: process.env.GROQAPIKEY, configuration: { baseURL: "https://api.groq.com/openai/v1", } }); const response = await model.invoke("Hello world"); console.log(response.text); ` ✔ Groq becomes a drop-in OpenAI replacement ✔ Only difference is the baseURL ✔ No separate provider wrapper required Why LangChain Is Better for Real Applications LangChain isn’t just a convenience upgrade—it solves the core architectural problems that appear the moment your project grows beyond a basic API call. Here’s why developers choose LangChain for production-grade LLM applications: Unified Model Interface Every LLM—OpenAI, Gemini, Groq, or others—uses the same method structure in LangChain. You no longer deal with: different endpoints different body formats different response parsing different role or message structures One interface. One way to call every provider. Minimal Boilerplate No more: fetch wrappers headers error handling JSON parsing token extraction nested response drilling LangChain abstracts all the plumbing so you focus on your application logic. Easy Provider Switching Want to swap Gemini → OpenAI → Groq? In raw APIs, that means rewriting half your file. In LangChain, it's: `tsx const model = new ChatOpenAI({ ... }); ` One line. Zero refactoring. Perfect for testing multiple models or building meta-LLM systems. Built-in RAG, Agents, Tools & Memory Adding advanced features without LangChain means building everything from scratch: embeddings vector stores document loading retrievers conversational memory multi-step agent workflows tool/function calling integrations LangChain gives you these components out-of-the-box—plug-and-play. Scales Better for Large Projects As your application grows, LangChain’s modular design keeps your code: clean organized readable maintainable extendable Instead of managing multiple provider wrappers and duplicated logic, you build on a unified, future-proof architecture. When Raw APIs Are Still Useful Even though LangChain simplifies most real-world AI development, there are still situations where calling LLMs directly through their native APIs is the better choice: Very Simple Prototypes If your app only sends a single prompt and returns a response, raw API calls are straightforward and require minimal setup. When You Need Extreme Low-Latency Direct API calls avoid the extra abstraction layer, making them ideal for latency-sensitive use cases like real-time inference or fast UI responses. When You Need Full Low-Level Control Some applications require custom request bodies, fine-tuned headers, or provider-specific features that LangChain abstracts away. When You're Building Something Very Custom If you're implementing unusual prompting flows, custom streaming logic, or tightly optimized pipelines, raw APIs give you more precise control. When You Prefer Zero Additional Dependencies For teams focused on minimalism, performance, or small bundle size, avoiding external frameworks can be a deliberate architectural choice. Conclusion Raw LLM APIs are excellent for quick experiments, testing ideas, or building simple one-off scripts. But as soon as your application grows—when you add multiple providers, expand features, introduce retrieval, or need maintainability—the limitations become obvious. LangChain was built to solve these exact problems. It eliminates: multi-provider integration headaches repetitive boilerplate inconsistent request and response structures manual memory and context handling fragile chaining logic complex RAG implementations you would otherwise build yourself With LangChain, your codebase becomes cleaner, your workflows become modular, and your application becomes significantly easier to scale and maintain over time. Instead of wrestling with provider differences, you can focus on delivering features—confident that your LLM layer is flexible, unified, and future-proof.
Learn how to create a type-safe REST API with Express and TypeScript, configured for seamless Vercel deployment. This comprehensive guide covers project setup, TypeScript configuration, API development, and deployment best practices. --Introduction In this guide, we’ll walk through building a simple REST API using Express with TypeScript, designed for easy deployment to Vercel. The API will manage a collection of blog posts stored in memory and include endpoints to create and retrieve posts. We’ll ensure type safety, configure environment variables, and follow modern backend development practices to create a scalable and maintainable project. This tutorial is perfect for developers looking to: Build a REST API with TypeScript and Express Ensure type safety for robust code Deploy a Node.js application to Vercel Understand modern backend development workflows The API includes the following features: Type safety with TypeScript interfaces Environment variable configuration Proper project structure for scalability Development tooling for a smooth workflow Vercel deployment configuration --Project Setup Initialize Node.js Project Start by creating a new Node.js project: `bash npm init -y && npm pkg set name="arfat.app" description="TypeScript Express API" author="Arfatur Rahman" ` This creates a package.json file with default values, setting the project name, description, and author. Install Runtime Dependencies Install the necessary runtime dependencies: `bash npm i express cors axios body-parser dotenv ` express: Web framework for Node.js cors: Middleware for enabling CORS axios: HTTP client for making requests body-parser: Middleware for parsing request bodies dotenv: Loads environment variables from a .env file Install Development Dependencies Add development dependencies for TypeScript and tooling: `bash npm i -D typescript ts-node-dev nodemon @types/express @types/node @types/cors ` typescript: TypeScript compiler ts-node-dev: Runs TypeScript files with hot reload nodemon: Watches for file changes and restarts the server @types/express: Type definitions for Express @types/node: Type definitions for Node.js @types/cors: Type definitions for CORS Initialize TypeScript Configuration Generate a tsconfig.json file to configure TypeScript: `bash npx tsc --init --rootDir src --outDir dist ` This sets the source files in the src directory and compiled output in the dist directory. --Project Structure Here’s the recommended project structure: ` project-root/ ├── src/ │ └── index.ts # Main application file ├── .env # Environment variables ├── .gitignore # Files to ignore in Git ├── package.json # Project configuration ├── tsconfig.json # TypeScript configuration └── vercel.json # Vercel deployment configuration ` --Key Files Explained tsconfig.json The tsconfig.json file configures the TypeScript compiler: `json { "compilerOptions": { "target": "es2016", "module": "commonjs", "rootDir": "src", "outDir": "dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, "include": ["src"], "exclude": ["nodemodules"] } ` Key settings: target: Compiles to ES2016 for modern JavaScript features module: Uses CommonJS for Node.js compatibility rootDir: Specifies src as the source directory outDir: Outputs compiled JavaScript to dist strict: Enables strict type checking for robust code esModuleInterop: Ensures compatibility with CommonJS modules package.json Scripts Update the scripts section in package.json to include: `json { "scripts": { "dev": "ts-node-dev --respawn --transpile-only --exit-child src/index.ts" } } ` The dev script runs the server in development mode with hot reloading and faster compilation. src/index.ts The main application file sets up the Express server and defines the API: `tsx import express, { Request, Response, Application } from "express"; import { randomBytes } from "crypto"; import bodyParser from "body-parser"; import dotenv from "dotenv"; import cors from "cors"; // Configuration dotenv.config(); const PORT = process.env.PORT || 4000; // Types interface Post { id: string; title: string; } interface Posts { [key: string]: Post; } // App State const posts: Posts = {}; // Express App Setup const app: Application = express(); app.use(bodyParser.json()); app.use(cors()); // Helper Functions const generateId = (): string => randomBytes(4).toString("hex"); // Controllers const getRoot = (req: Request, res: Response) => { res.send("Assalamualikum"); }; const getAllPosts = (req: Request, res: Response) => { res.json(posts); }; const createPost = (req: Request, res: Response) => { const id = generateId(); const { title } = req.body; if (!title) { res.status(400).json({ error: "Title is required" }); return; } posts[id] = { id, title }; res.status(201).json(posts[id]); }; // Routes app.get("/", getRoot); app.get("/posts", getAllPosts); app.post("/posts", createPost); // Server Initialization const startServer = async (): Promise<void> => { try { app.listen(PORT, () => { console.log(Server is running on port: ${PORT}); }); } catch (error) { console.error(Failed to start server: ${error}); process.exit(1); } }; startServer(); ` This file includes: Configuration: Loads environment variables and sets the default port Types: Defines Post and Posts interfaces for type safety State: Uses an in-memory object to store posts Express Setup: Configures middleware for JSON parsing and CORS Routes: Defines endpoints for the root, retrieving posts, and creating posts Server Initialization: Starts the server with error handling vercel.json The vercel.json file configures Vercel deployment: `json { "version": 2, "builds": [ { "src": "src/index.ts", "use": "@vercel/node" } ], "routes": [ { "src": "/(.)", "dest": "src/index.ts" } ] } ` This configuration: Specifies src/index.ts as the entry point Uses the @vercel/node builder for Node.js apps Routes all requests to the Express app .env and .gitignore Create a .env file for environment variables: ` PORT=4000 ` Create a .gitignore file to exclude sensitive files: ` .env nodemodules ` You can automate this with: ` echo -e 'PORT=4000' > .env && echo -e '.env\nnodemodules' > .gitignore ` --Development Workflow Create Configuration Files: Set up .env and .gitignore as shown above. Start the Development Server: ` npm run dev ` The server will: Run on the specified port (default: 4000) Automatically restart on file changes Log activity to the console --Deploying to Vercel Install Vercel CLI Install the Vercel CLI globally: ` npm i -g vercel ` Verify Installation Check the Vercel CLI version: ` npx vercel --version ` Log in to Vercel Authenticate with Vercel: ` npx vercel login ` Deploy the Application For a preview deployment: `bash npx vercel ` For a production deployment: ` npx vercel --prod ` --API Endpoints GET / Description: Returns a welcome message Response: "Assalamualikum" GET /posts Description: Retrieves all posts Response: JSON object containing all posts POST /posts Description: Creates a new post Body: { "title": "Post Title" } Response: JSON object of the created post with a generated ID Error: Returns 400 if title is missing --Type Safety TypeScript interfaces ensure type safety: ` interface Post { id: string; title: string; } interface Posts { [key: string]: Post; } ` Benefits include: Autocompletion in IDEs Compile-time type checking to catch errors early Clear documentation of data structures --Why Use TypeScript with Express? Using TypeScript with Express provides: Type Safety: Prevents runtime errors by catching type issues during compilation Improved Developer Experience: Autocompletion and IntelliSense in IDEs Scalability: Easier to maintain and extend as the codebase grows Better Refactoring: Type checking ensures safe code changes --Conclusion This guide demonstrated how to build a type-safe REST API using Express and TypeScript, configured for deployment to Vercel. By following modern backend development practices, including proper project structure, TypeScript configuration, and environment variable management, you can create a robust and scalable API. Deploying to Vercel ensures your API is accessible globally with minimal setup. Try building this API and deploying it yourself! For more advanced features, consider adding: A database (e.g., Supabase or MongoDB) for persistent storage Authentication middleware Request validation with libraries like Joi or zod API documentation with Swagger --About the Author Hi, I’m Arfatur Rahman, a Full-Stack Developer from Chittagong, Bangladesh, specializing in AI-powered applications, RAG-based chatbots, and scalable web platforms. I’ve worked with tools like Next.js, LangChain, OpenAI, Azure, and Supabase, building everything from real-time dashboards to SaaS products with payment integration. Passionate about web development, vector databases, and AI integration, I enjoy sharing what I learn through writing and open-source work. Connect with me: 🌐 Portfolio 💼 LinkedIn 👨💻 GitHub ✍️ Dev.to 📚 Medium
Elasticsearch is a robust, open-source search and analytics engine ideal for log analytics, full-text search, and large-scale data processing. This comprehensive guide walks you through installing, configuring, and running a single-node Elasticsearch instance on Windows, including setting it up as a Windows service, resetting passwords, and optimizing configurations for performance and security. This tutorial is designed for both beginners and experienced developers. Prerequisites Before starting, ensure you have: A Windows machine (Windows 10 or later recommended). Administrative access for installing services and modifying system files. A web browser to validate the Elasticsearch instance. Basic knowledge of Command Prompt or PowerShell. (Optional) Java Development Kit (JDK) if using a version of Elasticsearch that doesn’t bundle OpenJDK (versions 8.x and later typically include it). Step 1: Download and Install Elasticsearch Download Elasticsearch Visit the official Elasticsearch downloads page and download the latest Windows ZIP file (e.g., elasticsearch-9.x.x-windows-x8664.zip). Unzip the File Extract the ZIP file to the root of your C: drive (e.g., C:\elasticsearch-9.x.x) for simplicity. You can use tools like WinRAR, 7-Zip, or Windows’ built-in extraction feature. Example Path: C:\elasticsearch-9.x.x Step 2: Run Elasticsearch Navigate to the Elasticsearch Directory Open a Command Prompt or PowerShell with administrative privileges and navigate to the bin directory: `bash cd C:\elasticsearch-9.x.x\bin ` Run Elasticsearch Start Elasticsearch by running: `bash elasticsearch.bat ` The terminal will display startup logs. Look for a message indicating that Elasticsearch has started, along with the default password for the elastic user, such as: ` [INFO] Elasticsearch started The generated password for the elastic built-in superuser is: nuIwMi9fI8T3BYHu3mI ` Save this password for authentication. Note: Closing the terminal will stop Elasticsearch. To avoid this, consider running it as a Windows service (covered in Step 4). Step 3: Reset the Elastic User Password To secure your Elasticsearch instance, reset the default elastic user password: Ensure the Elasticsearch server is running. Open a new Command Prompt or PowerShell and navigate to the bin directory: `bash cd C:\elasticsearch-9.x.x\bin ` Run the password reset command: `bash elasticsearch-reset-password -i -u elastic ` When prompted, press y to confirm, then enter and re-enter your new password: ` This tool will reset the password of the [elastic] user to an autogenerated value. Proceed? [y/N] y Enter new password: Re-enter new password: Password for the [elastic] user successfully reset. New password: newpasswordhere ` Save the new password for future use. Step 4: Install Elasticsearch as a Windows Service Running Elasticsearch as a Windows service ensures it starts automatically with your system and operates in the background without a persistent terminal. Pros of Running as a Service ✅ Automatic Startup: Launches with Windows boot. ✅ Background Operation: No need for an open Command Prompt. ✅ Centralized Management: Control via Windows Services Manager (services.msc) or PowerShell. ✅ Production-Ready: Ideal for continuous operation. ✅ Integrated Logging: Logs are stored in the Windows Event Log for easy monitoring. Cons of Running as a Service ❌ Setup Complexity: Requires configuring Java paths and permissions. ❌ Permission Issues: The default LocalSystem account may face access issues if directories aren’t configured properly. ❌ Debugging Challenges: Errors appear in the Windows Event Log, not the console. ❌ Service Management: Updates require stopping and restarting the service. ❌ Memory Configuration: JVM heap settings must be pre-configured. Steps to Install Navigate to the bin directory: `bash cd C:\elasticsearch-9.x.x\bin ` Install the service: `bash elasticsearch-service.bat install ` A confirmation message will indicate successful installation. Manage the Service: Start the Service: `bash elasticsearch-service.bat start ` Stop the Service: `bash elasticsearch-service.bat stop ` Open Service Manager GUI: This opens a GUI to start, stop, or configure the service. `bash elasticsearch-service.bat manager ` Remove the Service (if needed): `bash elasticsearch-service.bat remove ` Verify Installation: Press Win R, type services.msc, and press Enter. Locate the Elasticsearch service and check its status (e.g., Running or Stopped). Step 5: Configure Elasticsearch Customize Elasticsearch settings by editing the elasticsearch.yml file in the config directory (e.g., C:\elasticsearch-9.x.x\config\elasticsearch.yml). Change Data Storage Path By default, Elasticsearch stores data in the data folder within its installation directory. To customize: `yaml path.data: ["C:\\elasticsearch-9.x.x\\data", "D:\\elasticdata"] ` Specify one or multiple paths for data storage. Ensure the directories exist and are writable. Remove any # before path.data to enable the setting. Set Memory Limits Elasticsearch’s default memory allocation (1 GB) may be insufficient for large datasets or multiple queries. To adjust: Navigate to the bin directory: `bash cd C:\elasticsearch-9.x.x\bin ` Open the service manager: `bash elasticsearch-service.bat manager ` In the GUI, go to the Java tab and set the Initial Memory Pool and Maximum Memory Pool to higher values (e.g., 10240 MB for 10 GB). Disable HTTPS (Optional) Elasticsearch uses HTTPS by default. To switch to HTTP for local testing: `yaml xpack.security.http.ssl: enabled: false ` Access Elasticsearch via http://localhost:9200/ after disabling SSL. Restrict Remote Access (Optional) Control who can access your Elasticsearch instance: `yaml http.host: 127.0.0.1 ` 127.0.0.1: Restricts access to localhost. 0.0.0.0: Allows access from any IP (use cautiously in production). Specify a LAN IP (e.g., 192.168.1.100) to limit access to a specific network. Change the Default Port (Optional) Elasticsearch uses port 9200 by default. To change it: `yaml http.port: 9200 ` Replace 9200 with your desired port. Apply Changes After editing elasticsearch.yml, save the file and restart the service: `bash elasticsearch-service.bat stop elasticsearch-service.bat start ` Step 6: Validate Elasticsearch To confirm Elasticsearch is running: Open a web browser and navigate to https://localhost:9200/ (or http://localhost:9200/ if HTTPS is disabled). Enter the credentials: Username: elastic Password: The password from Step 2 or the reset password from Step 3. A JSON response should appear, confirming the server is running: `json { "name": "your-node-name", "clustername": "elasticsearch", "version": { "number": "9.x.x", ... }, ... } ` If you see certificate warnings, this is normal for local HTTPS setups. Accept the warning or disable HTTPS as described above. Conclusion You’ve successfully installed, configured, and validated a single-node Elasticsearch instance on Windows! By running it as a service, resetting the elastic user password, and optimizing settings like data paths and memory limits, you’ve set up a robust environment for search and analytics. For advanced configurations, refer to the official Elasticsearch documentation. For a visual guide, check out this video tutorial (replace with the actual link if available). --About the Author Hi, I’m Arfatur Rahman, a Full-Stack Developer from Chittagong, Bangladesh, specializing in AI-powered applications, RAG-based chatbots, and scalable web platforms. I’ve worked with tools like Next.js, LangChain, OpenAI, Azure, and Supabase, building everything from real-time dashboards to SaaS products with payment integration. Passionate about web development, vector databases, and AI integration, I enjoy sharing what I learn through writing and open-source work. Connect with me: 🌐 Portfolio 💼 LinkedIn 👨💻 GitHub ✍️ Dev.to 📚 Medium
Introduction In modern web development, ensuring search engines properly index and crawl your website is crucial for SEO and visibility. Two essential files that help with this are sitemap.xml and robots.txt. If you're using Next.js with the App Router and TypeScript, you can dynamically generate these files to keep them updated as your content changes. In this guide, we'll walk through how to generate sitemap.xml and robots.txt in a Next.js project, why they are necessary, and how they improve your website's SEO. What is a Sitemap? A sitemap is an XML file that lists all the URLs of a website, providing search engines with a structured map of the site's content. This helps search engines like Google and Bing efficiently crawl and index pages, ensuring they appear in search results. Why is a Sitemap Required? Better Indexing: It ensures that search engines discover all important pages, even if they are not linked properly. Faster Crawling: A sitemap helps search engines prioritize and crawl new or updated content quickly. SEO Improvement: Providing metadata such as change frequency and priority helps search engines understand your content better. Generating sitemap.xml in Next.js To dynamically generate a sitemap in a Next.js project using TypeScript, we use the MetadataRoute.Sitemap API. Below is a complete implementation: `tsx import { db } from "@/lib/db"; import type { MetadataRoute } from "next"; export const revalidate = 60; const getDynamicSitemaps = ({ data, defaultRoute, }: { data: { slug: string }[]; defaultRoute: string; }) => { return data.map((d) => ({ url: ${process.env.NEXTPUBLICWEBSITEURL}/${defaultRoute}/${d.slug}, lastModified: new Date(), changeFrequency: "yearly" as const, priority: 1, })); }; export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const allBlogs = await db.blog.findMany(); const allProjects = await db.project.findMany(); const allProducts = await db.product.findMany(); const blogSiteMap = getDynamicSitemaps({ data: allBlogs, defaultRoute: "blogs", }); const projectSitemaps = getDynamicSitemaps({ data: allProjects, defaultRoute: "projects", }); const shopSitemaps = getDynamicSitemaps({ data: allProducts, defaultRoute: "shop", }); return [ { url: ${process.env.NEXTPUBLICWEBSITEURL}/, lastModified: new Date(), changeFrequency: "yearly", priority: 1, }, { url: ${process.env.NEXTPUBLICWEBSITEURL}/blogs, lastModified: new Date(), changeFrequency: "weekly", priority: 0.5, }, ...blogSiteMap, ...shopSitemaps, ...projectSitemaps, { url: ${process.env.NEXTPUBLICWEBSITEURL}/contact, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8, }, ]; } ` For more details, refer to the Next.js sitemap documentation. What is robots.txt? The robots.txt file is a text file that tells search engine crawlers which pages or sections of a website they can or cannot access. This is important for managing how search engines interact with your site. Why is robots.txt Required? Control Crawling: Prevent search engines from indexing private or unnecessary pages. Reduce Server Load: Block bots from crawling pages that do not need to be indexed. Improve SEO: Ensure search engines focus on important content rather than unnecessary pages. Generating robots.txt in Next.js In Next.js, we can define a robots.ts file inside the app directory to generate robots.txt dynamically: `tsx import type { MetadataRoute } from "next"; export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: "", allow: "/", disallow: "/dashboard/", }, sitemap: ${process.env.NEXTPUBLICWEBSITEURL}/sitemap.xml, }; } ` For more details, refer to the Next.js robots.txt documentation. Conclusion Generating sitemap.xml and robots.txt dynamically in a Next.js app router setup with TypeScript is a great way to ensure your website is optimized for search engines. The sitemap helps search engines discover and index pages efficiently, while the robots.txt file controls crawler access to ensure proper SEO strategies. By implementing these files, you improve the discoverability, indexing, and performance of your website in search engine rankings.
In this tutorial, I will guide you step-by-step on how to use Mistral AI's powerful mistral-large-latest model to build a custom agent that can track and respond to payment statuses and transaction dates. This tutorial assumes you have a basic understanding of JavaScript, Next.js, and how to work with APIs. By the end of this guide, you will know how to create a responsive agent that integrates with real-time data fetching functions. Prerequisites Before you begin, make sure you have: Basic knowledge of JavaScript, Node.js, and Next.js. A Mistral AI account for API access. Installed Next.js, Zod, and the Mistral client. If you haven't installed these yet, you can do so by running: `bash npx create-next-app@latest arfat cd arfat npm i @mistralai/mistralai zod ` Important links https://docs.mistral.ai/getting-started/quickstart/ ⇒ Docs https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.1 ⇒ Mistral in hugginface https://docs.mistral.ai/api/#tag/chat/operation/chatcompletionv1chatcompletionspost ⇒ Chat completion https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-mistral-chat-completion.html ⇒ Chat completion https://mistral.ai/en/products/la-plateforme#models ⇒ latest models https://mistral.ai/en/news/mixtral-of-experts ⇒ choose model as you required https://docs.mistral.ai/capabilities/embeddings/ ⇒ Embeddings Get API key https://auth.mistral.ai/ui/login ⇒ Sign in Create workspace if you’re new user Get Experimental Subscription https://console.mistral.ai/api-keys ⇒ Get API keys from here Mistral Free Tier Limitations 1 request per second: You can make one API request per second. 500,000 tokens per minute: The total number of tokens you can use in one minute. 1 billion tokens per month: The maximum token limit you can use in a month. These limitations are important to keep in mind while developing, so you can optimize how frequently you call the API to avoid exceeding the free tier limits. Schema and Input Validation We'll begin by validating the user input to ensure that the data passed to the Mistral AI model is in the correct format. Using Zod for validation, here's how you can ensure that the input text is a non-empty string: `tsx const inputSchema = z.object({ inputText: z .string({ invalidtypeerror: "Input text must be a string", // Error message if the input is not a string requirederror: "Input text is required", // Error message if the input is missing }) .min(1, { message: "Input text cannot be empty" }), // Ensures the string is not empty }); ` Defining Mistral Messages To interact with Mistral AI, we need to define a message interface that structures the conversation. The messages will have different roles such as system, user, assistant, and tool. `tsx export interface MistralMessage { role: "system" | "user" | "assistant" | "tool"; content: string; name?: string; // Optional name field toolCallId?: string; // Optional name field } ` This allows Mistral AI to process incoming data and respond with human-like messages. Setting Up the Mistral Client Mistral's client is an interface that allows you to send requests to the API. You'll need to set up the Mistral client with your API key and initialize the client in your code: `tsx import { Mistral } from "@mistralai/mistralai"; const apiKey = process.env.MISTRALAPIKEY; export const mistralClient = new Mistral({ apiKey: apiKey }); ` --Fetching Payment Data: Creating Helper Functions We'll simulate payment data for testing by creating a mock database. The transactionData array contains payment information, and we’ll use two functions to retrieve the payment status and date based on a transaction ID. `tsx const transactionData = [ { transactionid: "T1001", customerid: "C001", paymentamount: 125.5, paymentdate: "2021-10-05", paymentstatus: "Paid", }, ]; export function getPaymentStatus({ transactionId }: { transactionId: string }) { const transaction = transactionData.find( (row) => row.transactionid === transactionId ); if (transaction) return JSON.stringify({ status: transaction.paymentstatus }); return JSON.stringify({ error: "Transaction ID not found." }); } export function getPaymentDate({ transactionId }: { transactionId: string }) { const transaction = transactionData.find( (row) => row.transactionid === transactionId ); if (transaction) return JSON.stringify({ date: transaction.paymentdate }); return JSON.stringify({ error: "Transaction ID not found." }); } ` These functions will serve as "tools" for the assistant to fetch the required payment data. --Mistral Tools: Connecting Functions with the AI Model Mistral AI can call external functions (tools) to fetch data. Here, we define two tools: getPaymentStatus and getPaymentDate. `tsx import { Tool } from "@mistralai/mistralai/models/components"; const mistralTools: Tool[] = [ { type: "function", function: { name: "getPaymentStatus", description: "Get the payment status of a transaction", parameters: { type: "object", properties: { transactionId: { type: "string", description: "The transaction ID.", }, }, required: ["transactionId"], }, }, }, { type: "function", function: { name: "getPaymentDate", description: "Get the payment date of a transaction", parameters: { type: "object", properties: { transactionId: { type: "string", description: "The transaction ID.", }, }, required: ["transactionId"], }, }, }, ]; ` These tools will allow Mistral to call these functions when needed. The Main POST Handler In the main POST request handler, we process the user’s query and send it to Mistral AI. If Mistral determines that a tool is needed (e.g., to fetch the payment status or date), it will invoke the appropriate function. `tsx export async function POST(request: Request) { try { const body = await request.json(); const { inputText } = inputSchema.parse(body); const messages: MistralMessage[] = [ { role: "system", content: "You are a friendly assistant" }, { role: "user", content: inputText }, ]; const response = await mistralClient.chat.complete({ model: "mistral-large-latest", messages, toolChoice: "any", tools: mistralTools, temperature: 0.7, responseFormat: { type: "text" }, }); if (response?.choices && response.choices[0]?.message?.role === "assistant") { if (response?.choices[0].finishReason === "stop") { return formatResponse( { finalAnswer: response?.choices[0].message.content, ...response }, "Data fetched successfully" ); } if (response?.choices[0]?.finishReason === "toolcalls") { const toolCalls = response.choices[0]?.message?.toolCalls; if (toolCalls && toolCalls.length > 0) { const functionName = toolCalls[0]?.function?.name as keyof typeof availableFunctions; const functionArgs = JSON.parse(toolCalls[0]?.function?.arguments as string); const functionResponse = availableFunctionsfunctionName; messages.push({ role: "tool", name: functionName, content: functionResponse, toolCallId: toolCalls[0].id, }); await delay(3000); const functionREs = await mistralClient.chat.complete({ model: "mistral-large-latest", messages, }); return formatResponse(functionREs, "Data fetched successfully"); } } } } catch (error) { console.log("Error", { error }); return routeErrorHandler(error); } } ` Full Code `tsx import { formatResponse, routeErrorHandler } from "@/lib/api-response"; import { Mistral } from "@mistralai/mistralai"; // Import the Mistral client library to interact with Mistral AI export interface MistralMessage { role: "system" | "user" | "assistant" | "tool"; content: string; name?: string; // Optional name field toolCallId?: string; // Optional name field } / Schema for validating text input. This ensures that the input is a non-empty string. / export const inputSchema = z.object({ inputText: z .string({ invalidtypeerror: "Input text must be a string", // Error message if the input is not a string requirederror: "Input text is required", // Error message if the input is missing }) .min(1, { message: "Input text cannot be empty" }), // Ensures the string is not empty }); // Retrieve the Mistral API key from environment variables const apiKey = process.env.MISTRALAPIKEY; // Initialize the Mistral client with the API key (this is used to interact with Mistral's APIs) export const mistralClient = new Mistral({ apiKey: apiKey }); import { Tool } from "@mistralai/mistralai/models/components"; import { z } from "zod"; const transactionData = [ { transactionid: "T1001", customerid: "C001", paymentamount: 125.5, paymentdate: "2021-10-05", paymentstatus: "Paid", }, ]; export function getPaymentStatus({ transactionId }: { transactionId: string }) { const transaction = transactionData.find( (row) => row.customerid === transactionId ); if (transaction) return JSON.stringify({ status: transaction.paymentstatus }); return JSON.stringify({ error: "transaction id not found." }); } export function getPaymentDate({ transactionId }: { transactionId: string }) { const transaction = transactionData.find( (row) => row.transactionid === transactionId ); if (transaction) { return JSON.stringify({ date: transaction.paymentdate }); } return JSON.stringify({ error: "transaction id not found." }); } export const mistralTools: Tool[] = [ { type: "function", function: { name: "getPaymentStatus", description: "Get the payment date of a transaction", parameters: { type: "object", properties: { transactionId: { type: "string", description: "The transaction id.", }, }, required: ["transactionId"], }, }, }, { type: "function", function: { name: "getPaymentDate", description: "Get payment status of a transaction", parameters: { type: "object", properties: { transactionId: { type: "string", description: "The transaction id.", }, }, required: ["transactionId"], }, }, }, ]; // Available functions for tool calls const availableFunctions = { getPaymentDate, getPaymentStatus, } as const; // Using as const to make function names literal types // Helper function to create a delay (for waiting before next operation) const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); / Handles POST requests to interact with the Mistral AI model. @param request The incoming request object containing user input. @returns Formatted response or error message. / export async function POST(request: Request) { try { // Step 1: Parse and validate input text from the user request const body = await request.json(); const { inputText } = inputSchema.parse(body); // Validates input using Zod schema // Step 2: Prepare initial messages with system behavior and user input const messages: MistralMessage[] = [ { role: "system", // Defines the assistant's role and behavior content: "You are a friendly assistant", }, { role: "user", // User input message content: inputText, }, ]; // Step 3: Send messages to the Mistral chat model for a response const response = await mistralClient.chat.complete({ model: "mistral-large-latest", // AI model to generate the response messages, toolChoice: "any", // Allow any tool to be chosen tools: mistralTools, // Available tools for the assistant temperature: 0.7, // Controls the randomness of responses responseFormat: { type: "text", // Plain text response format }, }); // Step 4: Check if the assistant provided a valid response if ( response?.choices && response.choices[0]?.message?.role === "assistant" ) { // Add assistant's response to the messages messages.push(response.choices[0]?.message as MistralMessage); // Step 5: Handle completion based on the assistant's finish reason if (response?.choices[0].finishReason === "stop") { // If the assistant finishes the task, return the response return formatResponse( { finalAnswer: response?.choices[0].message.content, ...response, }, "Data fetched successfully" ); } else if ( response?.choices && response.choices[0]?.finishReason === "toolcalls" ) { // Handle tool calls if the assistant requires additional functions const toolCalls = response.choices[0]?.message?.toolCalls; if (toolCalls && toolCalls.length > 0) { // Extract the function name and arguments from tool calls const functionName = toolCalls[0]?.function ?.name as keyof typeof availableFunctions; // Ensures it's a valid function const functionArgs = JSON.parse( toolCalls[0]?.function?.arguments as string ); // Call the relevant function with arguments const functionResponse = availableFunctionsfunctionName; // Add tool response to messages messages.push({ role: "tool", name: functionName, content: functionResponse, toolCallId: toolCalls[0].id, }); // Wait for a short delay before continuing await delay(3000); // Step 6: Send the updated messages back to the Mistral model const functionREs = await mistralClient.chat.complete({ model: "mistral-large-latest", messages, }); // Return the response after the tool function execution return formatResponse(functionREs, "Data fetched successfully"); } // Default response if no special conditions were met return formatResponse(response, "Data fetched successfully"); } } } catch (error) { // Log and return an error response if any step fails console.log("Error", { error }); return routeErrorHandler(error); } } ` Conclusion With this approach, we’ve successfully built a custom agent that can interact with Mistral AI to fetch real-time data. We used Mistral AI’s mistral-large-latest model, along with helper functions and tools, to create an intelligent agent capable of answering user queries about payment status and dates. Useful Links: Visit my portfolio: arfat.app GitHub: https://github.com/arfat-xyz LinkedIn: https://www.linkedin.com/in/arfat-rahman/ Email: [email protected]