Copy-paste recipes for AI development. All TypeScript. All tested patterns.
22 snippets across 6 categories
22 snippets
Basic streaming text generation using the AI SDK. Returns a ReadableStream you can pipe to the client.
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant.",
prompt: "Explain React Server Components in 3 sentences.",
});
// Pipe to a Response (Route Handler / Server Action)
return result.toDataStreamResponse();Generate type-safe structured data from an LLM using a Zod schema. No parsing or validation needed.
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
name: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
prepTime: z.number().describe("Prep time in minutes"),
}),
prompt: "Generate a recipe for chocolate chip cookies.",
});
console.log(object.name);
// "Classic Chocolate Chip Cookies"Define tools the model can call. The SDK handles the tool call loop automatically.
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const result = await generateText({
model: openai("gpt-4o"),
tools: {
weather: tool({
description: "Get the current weather for a location",
parameters: z.object({
city: z.string().describe("The city name"),
}),
execute: async ({ city }) => {
// Call your weather API here
return { city, temp: 72, condition: "sunny" };
},
}),
},
maxSteps: 5,
prompt: "What's the weather in San Francisco?",
});
console.log(result.text);Full chat UI in a few lines. Handles streaming, message history, loading state, and error handling.
"use client";
import { useChat } from "@ai-sdk/react";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({ api: "/api/chat" });
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Say something..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
);
}Send a message to Claude using the Anthropic SDK. Supports system prompts, multi-turn conversations, and streaming.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const message = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: "You are a senior TypeScript developer.",
messages: [
{ role: "user", content: "Write a debounce function with generics." },
],
});
console.log(message.content[0].type === "text" && message.content[0].text);Define tools for Claude to call. Handle the tool_use response and feed results back in a conversation loop.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
tools: [
{
name: "get_stock_price",
description: "Get the current stock price for a ticker symbol",
input_schema: {
type: "object" as const,
properties: {
ticker: { type: "string", description: "Stock ticker symbol" },
},
required: ["ticker"],
},
},
],
messages: [{ role: "user", content: "What's the price of AAPL?" }],
});
// Check if Claude wants to use a tool
for (const block of response.content) {
if (block.type === "tool_use") {
console.log(`Tool: ${block.name}, Input: ${JSON.stringify(block.input)}`);
}
}Send images to Claude for analysis. Supports base64-encoded images and URLs.
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync } from "fs";
const client = new Anthropic();
const imageData = readFileSync("screenshot.png").toString("base64");
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [
{
role: "user",
content: [
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: imageData,
},
},
{
type: "text",
text: "Describe this UI. List any accessibility issues you see.",
},
],
},
],
});
console.log(response.content[0].type === "text" && response.content[0].text);Stream responses token-by-token from Claude. Uses server-sent events under the hood.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a haiku about TypeScript." }],
});
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
process.stdout.write(event.delta.text);
}
}
const finalMessage = await stream.finalMessage();
console.log("\nTokens used:", finalMessage.usage.input_tokens);Create an MCP server with a custom tool. Uses the official TypeScript SDK with stdio transport.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0",
});
server.tool(
"search_docs",
"Search documentation by keyword",
{ query: z.string(), limit: z.number().optional().default(10) },
async ({ query, limit }) => {
const results = await searchIndex(query, limit);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);Expose data as an MCP resource that AI clients can read. Resources are like GET endpoints for LLMs.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "project-context",
version: "1.0.0",
});
// Static resource
server.resource("readme", "file:///readme", async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text: "# My Project\nThis is the project README.",
},
],
}));
// Dynamic resource with template
server.resource(
"user-profile",
"users://{userId}/profile",
async (uri, { userId }) => {
const user = await db.getUser(userId);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(user),
},
],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);Create reusable prompt templates that AI clients can discover and use. Prompts are like stored procedures for LLMs.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "prompts",
version: "1.0.0",
});
server.prompt(
"code-review",
"Review code for bugs, security issues, and style",
{ language: z.string(), code: z.string() },
({ language, code }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: [
`Review this ${language} code.`,
"Check for: bugs, security issues, performance, readability.",
"Format: list each issue with severity (high/medium/low).",
"",
`\`\`\`${language}`,
code,
"\`\`\`",
].join("\n"),
},
},
],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);A POST route handler that streams AI responses. Drop this in app/api/chat/route.ts.
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful coding assistant.",
messages,
});
return result.toDataStreamResponse();
}Call an LLM from a Server Action and stream the result to the client using createStreamableValue.
// app/actions.ts
"use server";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createStreamableValue } from "ai/rsc";
export async function generate(prompt: string) {
const stream = createStreamableValue("");
(async () => {
const result = streamText({
model: openai("gpt-4o"),
prompt,
});
for await (const delta of result.textStream) {
stream.update(delta);
}
stream.done();
})();
return { output: stream.value };
}Render AI-generated content as it streams in. Uses useChat for real-time updates with a loading indicator.
"use client";
import { useChat } from "@ai-sdk/react";
export default function StreamingChat() {
const { messages, input, handleInputChange, handleSubmit, status } =
useChat();
return (
<div className="max-w-2xl mx-auto p-4">
<div className="space-y-4 mb-4">
{messages.map((m) => (
<div
key={m.id}
className={m.role === "user" ? "text-right" : "text-left"}
>
<div
className={`inline-block p-3 rounded-lg ${
m.role === "user"
? "bg-black text-white"
: "bg-gray-100 text-black"
}`}
>
{m.content}
</div>
</div>
))}
{status === "streaming" && (
<div className="text-gray-400 animate-pulse">Thinking...</div>
)}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
className="flex-1 border rounded-full px-4 py-2"
placeholder="Ask something..."
/>
<button
type="submit"
className="bg-black text-white px-6 py-2 rounded-full"
>
Send
</button>
</form>
</div>
);
}Simple in-memory rate limiter for AI API routes. Prevents abuse and controls costs.
// lib/rate-limit.ts
const rateLimit = new Map<string, { count: number; resetTime: number }>();
export function checkRateLimit(
ip: string,
limit = 10,
windowMs = 60_000
): { allowed: boolean; remaining: number } {
const now = Date.now();
const entry = rateLimit.get(ip);
if (!entry || now > entry.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + windowMs });
return { allowed: true, remaining: limit - 1 };
}
if (entry.count >= limit) {
return { allowed: false, remaining: 0 };
}
entry.count++;
return { allowed: true, remaining: limit - entry.count };
}
// Usage in route handler:
// const ip = req.headers.get("x-forwarded-for") ?? "unknown";
// const { allowed } = checkRateLimit(ip);
// if (!allowed) return new Response("Too many requests", { status: 429 });Call an LLM from a Convex action and store the result in the database. Actions can make external API calls.
// convex/ai.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import Anthropic from "@anthropic-ai/sdk";
export const summarize = action({
args: { text: v.string(), documentId: v.id("documents") },
handler: async (ctx, { text, documentId }) => {
const client = new Anthropic();
const message = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 512,
messages: [
{
role: "user",
content: `Summarize this in 2-3 sentences:\n\n${text}`,
},
],
});
const summary =
message.content[0].type === "text" ? message.content[0].text : "";
// Store the summary back in the database
await ctx.runMutation(api.documents.updateSummary, {
id: documentId,
summary,
});
return summary;
},
});Schedule a recurring AI task using Convex cron jobs. Runs a daily content digest.
// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
// Run every day at 9am UTC
crons.daily(
"daily-digest",
{ hourUTC: 9, minuteUTC: 0 },
internal.digest.generateDailyDigest
);
export default crons;
// convex/digest.ts
import { internalAction } from "./_generated/server";
import { internal } from "./_generated/api";
export const generateDailyDigest = internalAction({
handler: async (ctx) => {
// Fetch recent items from the database
const items = await ctx.runQuery(internal.items.getRecent, {
since: Date.now() - 86_400_000,
});
// Generate digest with AI
const digest = await generateWithAI(
`Create a brief digest of these ${items.length} items: ${JSON.stringify(items)}`
);
// Store the digest
await ctx.runMutation(internal.digests.store, {
content: digest,
date: new Date().toISOString().slice(0, 10),
});
},
});Semantic search with Convex vector indexes. Embed documents and query by similarity for RAG pipelines.
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
documents: defineTable({
title: v.string(),
content: v.string(),
embedding: v.array(v.float64()),
}).vectorIndex("by_embedding", {
vectorField: "embedding",
dimensions: 1536,
}),
});
// convex/search.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";
export const searchSimilar = action({
args: { query: v.string(), limit: v.optional(v.number()) },
handler: async (ctx, { query, limit = 5 }) => {
// Generate embedding for the query
const embedding = await getEmbedding(query);
// Search by vector similarity
const results = await ctx.vectorSearch("documents", "by_embedding", {
vector: embedding,
limit,
});
return results;
},
});Quick token estimation without external dependencies. Good enough for context window management and cost estimates.
function estimateTokens(text: string): number {
// GPT/Claude models average ~4 characters per token for English text.
// This is a rough estimate - use tiktoken for exact counts.
return Math.ceil(text.length / 4);
}
function estimateCost(
inputTokens: number,
outputTokens: number,
model: "gpt-4o" | "claude-sonnet" | "claude-haiku"
): number {
const pricing = {
"gpt-4o": { input: 2.5 / 1_000_000, output: 10 / 1_000_000 },
"claude-sonnet": { input: 3 / 1_000_000, output: 15 / 1_000_000 },
"claude-haiku": { input: 0.25 / 1_000_000, output: 1.25 / 1_000_000 },
};
const p = pricing[model];
return inputTokens * p.input + outputTokens * p.output;
}
// Usage
const text = "Hello, how are you doing today?";
const tokens = estimateTokens(text);
const cost = estimateCost(tokens, 200, "claude-sonnet");
console.log(`~${tokens} input tokens, estimated cost: $${cost.toFixed(4)}`);Build reusable prompt templates with typed variables. Catches missing variables at compile time.
type PromptVars<T extends string> = Record<T, string>;
function createPrompt<T extends string>(
template: string,
vars: PromptVars<T>
): string {
return Object.entries<string>(vars).reduce(
(prompt, [key, value]) =>
prompt.replaceAll(`{{${key}}}`, value),
template
);
}
// Define templates
const templates = {
codeReview: `You are a senior {{language}} developer.
Review this code for bugs, security issues, and performance.
\`\`\`{{language}}
{{code}}
\`\`\`
Focus on: {{focus}}`,
summarize: `Summarize the following {{contentType}} in {{length}} sentences.
Audience: {{audience}}.
{{content}}`,
} as const;
// Usage
const prompt = createPrompt(templates.codeReview, {
language: "TypeScript",
code: "const data = JSON.parse(userInput);",
focus: "security vulnerabilities",
});Wrap any async function with automatic retries and exponential backoff. Essential for flaky AI API calls.
async function withRetry<T>(
fn: () => Promise<T>,
opts: { maxRetries?: number; baseDelay?: number; maxDelay?: number } = {}
): Promise<T> {
const { maxRetries = 3, baseDelay = 1000, maxDelay = 30_000 } = opts;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: unknown) {
if (attempt === maxRetries) throw error;
// Don't retry client errors (4xx except 429)
if (error instanceof Error && "status" in error) {
const status = (error as { status: number }).status;
if (status >= 400 && status < 500 && status !== 429) throw error;
}
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
const jitter = delay * (0.5 + Math.random() * 0.5);
await new Promise((r) => setTimeout(r, jitter));
}
}
throw new Error("Unreachable");
}
// Usage
const response = await withRetry(
() => client.messages.create({ model: "claude-sonnet-4-20250514", max_tokens: 1024, messages }),
{ maxRetries: 3, baseDelay: 1000 }
);Comprehensive error handling for AI API calls. Covers rate limits, context length, and network errors.
interface AIError {
type: "rate_limit" | "context_length" | "auth" | "server" | "network" | "unknown";
message: string;
retryable: boolean;
retryAfter?: number;
}
function classifyError(error: unknown): AIError {
if (error instanceof Error && "status" in error) {
const status = (error as { status: number }).status;
const msg = error.message;
if (status === 429) {
return {
type: "rate_limit",
message: "Rate limit exceeded. Slow down requests.",
retryable: true,
retryAfter: 60,
};
}
if (status === 400 && msg.includes("context")) {
return {
type: "context_length",
message: "Input too long. Reduce prompt or conversation history.",
retryable: false,
};
}
if (status === 401 || status === 403) {
return {
type: "auth",
message: "Invalid or expired API key.",
retryable: false,
};
}
if (status >= 500) {
return {
type: "server",
message: "AI provider is having issues. Try again shortly.",
retryable: true,
retryAfter: 5,
};
}
}
if (error instanceof TypeError && (error.message.includes("fetch") || error.message.includes("network"))) {
return { type: "network", message: "Network error.", retryable: true };
}
return { type: "unknown", message: String(error), retryable: false };
}New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.