cd /news/ai-agents/building-a-production-ai-agent-in-ty… · home topics ai-agents article
[ARTICLE · art-58099] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Building a production AI agent in TypeScript with Mastra: a 2026 step-by-step.

A developer built a production AI agent in TypeScript using the Mastra framework, reducing code from 400 lines with the raw Anthropic SDK to about 60 lines. Mastra, a TypeScript-first agent framework with 24k+ GitHub stars and 88 releases as of May 2026, provides a model router supporting 40+ providers and includes scaffolding, custom tools, and persistent memory. The tutorial demonstrates creating an agent with a system prompt, custom tool, memory, and deployment in under an hour.

read9 min views1 publishedJul 13, 2026

I spent an afternoon last month wiring up an AI agent in raw TypeScript using the Anthropic SDK directly. The code worked, but I owned every piece of it: the tool dispatch loop, the conversation history array, the retry logic. Around 400 lines before the agent did anything interesting.

Mastra cuts that to about 60. A TypeScript-first agent framework with 24k+ GitHub stars, an active release cadence (88 releases as of May 2026), and a model router that talks to 40+ providers through one API. This tutorial goes from zero to a running agent with a custom tool and persistent memory. All code in this article runs.

Step What you build Time
Install Scaffolded project with Mastra wired in 5 min
Agent An agent with a system prompt and a model 10 min
Tool A custom tool the agent calls 15 min
Memory Conversation history across sessions 10 min
Deploy A running HTTP server 5 min

Raw SDK calls give you full control but you write the orchestration layer yourself: the tool call loop, history management, error handling, retries. Frameworks like LangChain and LlamaIndex solve this but lean heavily Python-first; the TypeScript ports lag the Python versions.

Mastra starts from TypeScript. The primitives map directly to what TypeScript developers already know: classes, Zod schemas, async functions. No port-lag exists because the framework itself is the TypeScript version.

The trade-off is the same as any framework: you trade control for speed. For prototypes and most production agents, the trade is worth it. For cases where the framework's tool dispatch or memory behavior does not match your exact needs, you can always drop a layer and call the Vercel AI SDK directly, which Mastra wraps under the hood.

Mastra's scaffolder creates everything you need:

npm create mastra@latest

The wizard asks for a project name, model provider, and whether you want the starter example files. Pick your provider (OpenAI, Anthropic, Google, or any of the 40+ supported). For this tutorial I'll use Anthropic.

After the wizard finishes:

cd my-agent
npm install

Open .env

and add your key:

ANTHROPIC_API_KEY=sk-ant-...

The generated project structure looks like this:

src/
  mastra/
    index.ts          # Mastra instance
    agents/
      weather-agent.ts
    tools/
      weather-tool.ts

You can rename or replace the starter files. The root entry point src/mastra/index.ts

registers your agents and tools with the Mastra runtime:

// src/mastra/index.ts
import { Mastra } from "@mastra/core";
import { supportAgent } from "./agents/support-agent";

export const mastra = new Mastra({
  agents: { supportAgent },
});

That is the full configuration. No YAML, no config files, no factory functions to memorize.

An agent in Mastra is an instance of the Agent

class. You give it an id

, a name

, a model

, and instructions

. The instructions are the system prompt.

// src/mastra/agents/support-agent.ts
import { Agent } from "@mastra/core/agent";

export const supportAgent = new Agent({
  id: "support-agent",
  name: "Support Agent",
  model: "anthropic/claude-sonnet-4-6-20250929",
  instructions: `You are a support agent for a SaaS product.
Answer questions clearly. If you do not know the answer, say so.
When the user describes a bug, ask for their account ID and browser version before troubleshooting.
Keep responses under 150 words unless the user asks for detail.`,
});

The model

field uses Mastra's router format: provider/model-id

. This means swapping from Anthropic to OpenAI is a one-line change. Pin the model ID to a dated version, not an alias, so you own when it upgrades.

To call the agent from your application code:

import { mastra } from "./mastra";

const agent = mastra.getAgentById("support-agent");

const response = await agent.generate("My dashboard stopped  after your last update.");
console.log(response.text);

generate

returns a full response after the model finishes. If you want streaming for a chat UI, swap to stream

:

const stream = await agent.stream("What does the onboarding flow look like?");
for await (const chunk of stream.textStream) {
  process.stdout.write(chunk);
}

Both methods return { text, toolCalls, toolResults, usage }

. The usage

object gives you token counts per call, which you should log in production.

Tools are how agents take action beyond text generation. A tool has an id

, a description

the model uses to decide when to call it, an inputSchema

validated by Zod, and an execute

function.

Here is a tool that looks up an account by ID from a hypothetical database:

// src/mastra/tools/account-lookup.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const accountLookupTool = createTool({
  id: "account-lookup",
  description: "Look up a customer account by their account ID. Returns plan, status, and creation date.",
  inputSchema: z.object({
    accountId: z.string().describe("The customer account ID, format ACC-XXXXXXXX"),
  }),
  execute: async ({ context }) => {
    const { accountId } = context;
    // Replace with your real DB call
    const account = await fetchAccountFromDb(accountId);
    if (!account) {
      return { found: false, accountId };
    }
    return {
      found: true,
      accountId,
      plan: account.plan,
      status: account.status,
      createdAt: account.createdAt,
    };
  },
});

async function fetchAccountFromDb(accountId: string) {
  // Stub — wire to your actual data layer
  if (accountId === "ACC-00000001") {
    return { plan: "pro", status: "active", createdAt: "2025-03-15" };
  }
  return null;
}

Attach the tool to the agent by adding it to the tools

array:

// src/mastra/agents/support-agent.ts
import { Agent } from "@mastra/core/agent";
import { accountLookupTool } from "../tools/account-lookup";

export const supportAgent = new Agent({
  id: "support-agent",
  name: "Support Agent",
  model: "anthropic/claude-sonnet-4-6-20250929",
  instructions: `You are a support agent for a SaaS product.
When a user provides an account ID, use the account-lookup tool to retrieve their account details before answering.
Answer questions clearly. If you do not know the answer, say so.`,
  tools: {
    "account-lookup": accountLookupTool,
  },
});

The model now sees the tool in its context. When a user says "My account ACC-00000001 is showing the wrong plan", the agent calls account-lookup

, gets the account record back, and includes it in its reasoning before responding. You see the tool call in response.toolCalls

and the result in response.toolResults

.

The Zod schema does two things: it tells the model what the tool expects (the description and field names show up in the model's tool spec), and it validates the model's output before your execute

function runs. If the model sends a malformed accountId

, Mastra rejects the call before it hits your code.

Without memory, every call to agent.generate

starts from a blank slate. The agent does not know what the user said two messages ago. For a support agent this breaks multi-turn conversations immediately.

Mastra's @mastra/memory

package adds three layers: message history (the last N turns), working memory (key facts extracted and stored between turns), and semantic recall (vector search over past conversations). For most agents, message history is enough to start.

Install the package and a storage backend:

npm install @mastra/memory @mastra/libsql

Wire memory into the agent:

// src/mastra/agents/support-agent.ts
import { Agent } from "@mastra/core/agent";
import { Memory } from "@mastra/memory";
import { LibSQLStore } from "@mastra/libsql";
import { accountLookupTool } from "../tools/account-lookup";

const memory = new Memory({
  storage: new LibSQLStore({
    url: process.env.DATABASE_URL ?? "file:./agent.db",
  }),
  options: {
    lastMessages: 20,
  },
});

export const supportAgent = new Agent({
  id: "support-agent",
  name: "Support Agent",
  model: "anthropic/claude-sonnet-4-6-20250929",
  instructions: `You are a support agent for a SaaS product.
When a user provides an account ID, use the account-lookup tool before answering.
Keep responses under 150 words unless the user asks for detail.`,
  tools: {
    "account-lookup": accountLookupTool,
  },
  memory,
});

Pass a resource

and thread

when you call the agent. resource

identifies the user; thread

isolates a specific conversation:

const response = await agent.generate(
  "What plan am I on?",
  {
    memory: {
      resource: "user-42",
      thread: "support-session-2026-05-25",
    },
  },
);

On the first turn the agent has no history. On the second turn it gets the last 20 messages from that resource and thread. The user can say "follow up from earlier" and the agent knows what earlier means.

LibSQL writes to a local SQLite file in development. For production, point DATABASE_URL

at a Turso connection string and the same code writes to a distributed SQLite database with no other changes.

Development server:

npx mastra dev

This starts a local server with a REST API over your agents and a browser-based studio on port 4111 where you can send messages to the agent, inspect tool calls, and read the memory state. The studio is useful enough that I use it even for non-UI agents.

For production you have two options.

The first is the Mastra cloud deploy, which packages your agents as a managed service:

npx mastra deploy

The second is self-hosting. Add @mastra/express

and mount the Mastra server into your existing Express app:

npm install @mastra/express
python
// src/index.ts
import express from "express";
import { MastraServer } from "@mastra/express";
import { mastra } from "./mastra";

const app = express();
app.use(express.json());

const server = new MastraServer({ app, mastra });
await server.init();

app.listen(process.env.PORT ?? 3000);

MastraServer.init()

registers the agent endpoints under /api

. Your agents become callable over HTTP with no extra routing code. Deploy this to Railway, Fly.io, or any Node.js host that accepts a Dockerfile

.

Mastra removes the orchestration scaffolding that accounts for the first 300 lines of every TypeScript agent project. The three hours I spent on tool dispatch and conversation history management with raw SDK calls collapse to about 15 minutes with Mastra.

The framework makes a bet on TypeScript as the language where agents actually ship to production, not just where they get prototyped. That bet reflects what I see in production codebases. Python still dominates training and research. TypeScript dominates the web services those agents plug into.

The main thing I'd add to the stack described above: log response.usage

on every generate call so you can see what the agent actually costs per session in production. The number surprises most teams the first week.

What does your agent stack look like right now? Raw SDK, a framework, something in-house? Curious what the pressure points are.

GDS K S · thegdsks.com · follow on X @thegdsks

The orchestration layer is the part nobody talks about until they've rewritten it twice.

── more in #ai-agents 4 stories · sorted by recency
── more on @mastra 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-a-productio…] indexed:0 read:9min 2026-07-13 ·