ai-web-apps/SKILL.md
Building AI-enhanced web apps — MCP servers/clients, multi-provider AI factory, streamUI with tool calling generators, composable middleware, per-user quotas, model fallback, multi-modal images, streaming, RAG, structured output, prompt...
npx skillsauth add peterbamuhigire/skills-web-dev ai-web-appsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
ai-web-apps or would be better handled by a more specific companion skill.SKILL.md first, then load only the referenced deep-dive files that are necessary for the task.world-class-engineering for release gates and production-quality expectations.frontend-performance for Core Web Vitals and budgets.vibe-security-skill and ai-security for threat modeling, abuse controls, and secure defaults.api-design-first when the AI app exposes or depends on external contracts.Specify before coding:
User (React UI)
↓ HTTP / Server Actions
Next.js App Router
↓ Vercel AI SDK
AI Providers (OpenAI, Google Gemini, Anthropic)
↓ MCP Tools / LangChain.js
External APIs + Vector Stores
npm install ai @ai-sdk/openai @ai-sdk/google @ai-sdk/anthropic
MCP is a standardized protocol for exposing tools to AI models across applications.
// src/stdio/server.js
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-mcp', version: '1.0.0' });
server.tool('get-data', 'Fetch data from internal API', {}, async () => {
const data = await fetchInternalData();
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
});
const transport = new StdioServerTransport();
await server.connect(transport);
// app/api/chat/route.ts
import { streamText, convertToModelMessages, experimental_createMCPClient } from 'ai';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio';
export async function POST(req: Request) {
const { messages } = await req.json();
const transport = new StdioClientTransport({ command: 'node', args: ['src/stdio/server.js'] });
const mcpClient = await experimental_createMCPClient({ transport });
const tools = await mcpClient.tools();
const result = streamText({
model: gemini('gemini-2.5-flash'),
messages: convertToModelMessages(messages),
tools,
onFinish: async () => await mcpClient.close(),
});
return result.toUIMessageStreamResponse();
}
// lib/ai-factory.ts
import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createAnthropic } from '@ai-sdk/anthropic';
const providers = {
openai: { constructor: createOpenAI, models: ['gpt-3.5-turbo', 'gpt-4', 'gpt-4o'] },
gemini: { constructor: createGoogleGenerativeAI, models: ['models/gemini-2.0-flash', 'models/gemini-2.5-flash'] },
anthropic: { constructor: createAnthropic, models: ['claude-opus-4-6', 'claude-sonnet-4-6'] },
};
export function getSupportedModel(provider: string, model: string) {
const cfg = providers[provider as keyof typeof providers];
if (!cfg) throw new Error(`Unsupported provider: ${provider}`);
if (!cfg.models.includes(model)) throw new Error(`Unsupported model: ${model}`);
const apiKey = process.env[`${provider.toUpperCase()}_API_KEY`];
if (!apiKey) throw new Error(`Missing API key for: ${provider}`);
return cfg.constructor({ apiKey })(model);
}
// Usage: const model = getSupportedModel('gemini', 'models/gemini-2.0-flash');
// app/api/chat/route.ts
export async function POST(req: Request) {
const { messages, provider = 'gemini', model = 'models/gemini-2.0-flash' } = await req.json();
const result = await streamText({
model: getSupportedModel(provider, model),
system: 'You are a helpful assistant.',
messages,
maxTokens: 1024,
});
return result.toDataStreamResponse();
}
export const maxDuration = 30;
'use client';
import { useChat } from 'ai/react';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
body: { provider: 'gemini', model: 'models/gemini-2.0-flash' },
});
return (
<div className="flex flex-col h-screen">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(m => (
<div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}>
<span className="rounded bg-gray-100 px-3 py-2 inline-block">{m.content}</span>
</div>
))}
{isLoading && <span className="animate-pulse">Thinking...</span>}
</div>
<form onSubmit={handleSubmit} className="p-4 border-t flex gap-2">
<textarea value={input} onChange={handleInputChange} className="flex-1 border rounded p-2" />
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}
import { streamUI } from 'ai/rsc';
import { z } from 'zod';
export async function streamWeatherUI(input: string) {
const result = await streamUI({
model: getSupportedModel('openai', 'gpt-4'),
messages: [{ role: 'user', content: input }],
text: ({ content }) => <ChatBubble text={content} />,
tools: {
getWeather: {
description: 'Get current weather for a city',
parameters: z.object({ city: z.string() }),
// Async generator — yields interim UI, returns final component
generate: async function* ({ city }) {
yield <LoadingSpinner city={city} />;
const weather = await fetchWeather(city);
return <WeatherCard city={city} temp={weather.temp} condition={weather.condition} />;
},
},
},
});
return { display: result.value };
}
// middleware.ts
import { NextResponse } from 'next/server';
const composeMiddleware = (middlewares: Function[]) => async (request: Request) => {
for (const fn of middlewares) {
const result = await fn(request);
if (result?.response) return result.response;
if (result?.continue === false) break;
}
return NextResponse.next();
};
const handleCORS = async (req: Request) => {
const origin = req.headers.get('origin');
if (origin && !allowedOrigins.includes(origin))
return { response: new Response('CORS error', { status: 403 }) };
};
const rateLimit = async (req: Request) => {
const { success } = await upstashRatelimit.limit(req.ip || '127.0.0.1');
if (!success) return { response: new Response('Too many requests', { status: 429 }) };
};
const authenticate = async (req: Request) => {
const token = req.headers.get('authorization')?.split(' ')[1];
if (!token) return { response: new Response('Unauthorized', { status: 401 }) };
};
export default composeMiddleware([handleCORS, rateLimit, authenticate]);
export const config = { matcher: '/api/:path*' };
// lib/quota.ts
import redis from './redis';
export async function checkMessageQuota(userId: string, dailyLimit = 10): Promise<boolean> {
const today = new Date().toISOString().split('T')[0];
const key = `quota:${userId}:${today}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 24 * 60 * 60); // TTL: 24h
return count <= dailyLimit;
}
// In API route
const { userId } = getAuth(req);
if (!await checkMessageQuota(userId, 10)) {
return Response.json({ error: 'Daily message quota exceeded (10/day)' }, { status: 429 });
}
import { createFallback } from 'ai-fallback';
import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
const model = createFallback({
models: [
createGoogleGenerativeAI({ apiKey: process.env.GEMINI_API_KEY! })('models/gemini-2.0-flash'),
createOpenAI({ apiKey: process.env.OPENAI_API_KEY! })('gpt-3.5-turbo'),
],
onError: (error, modelId) => console.error(`Model ${modelId} failed:`, error),
shouldRetryThisError: (error) => [429, 500, 503].includes(error.statusCode),
modelResetInterval: 60_000,
});
// Backend: process image + text together
const processMessages = (messages: Message[], imageData?: { base64: string; mimeType: string }) => {
if (!imageData || !messages.length) return messages;
const last = messages[messages.length - 1];
if (last.role === 'user') {
last.content = [
{ type: 'text', text: typeof last.content === 'string' ? last.content : '' },
{ type: 'image', image: `data:${imageData.mimeType};base64,${imageData.base64}` },
];
}
return messages;
};
// Frontend: file → base64
const handleFileUpload = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => setImageData({
base64: (e.target?.result as string).split(',')[1],
mimeType: file.type,
});
reader.readAsDataURL(file);
};
import { generateObject } from 'ai';
import { z } from 'zod';
const { object } = await generateObject({
model: getSupportedModel('openai', 'gpt-4'),
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
sentiment: z.enum(['positive', 'negative', 'neutral']),
summary: z.string().max(200),
}),
prompt: 'Analyse this review: "Great product, fast shipping!"',
});
// object.title, object.tags — fully typed
import { streamText, tool } from 'ai';
import { z } from 'zod';
const result = await streamText({
model: getSupportedModel('openai', 'gpt-4'),
tools: {
getWeather: tool({
description: 'Get current weather for a city',
parameters: z.object({ city: z.string(), unit: z.enum(['celsius', 'fahrenheit']).default('celsius') }),
execute: async ({ city }) => ({ temperature: 22, condition: 'Sunny', city }),
}),
},
messages: [{ role: 'user', content: "What's the weather in London?" }],
});
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, number>({ max: 500, ttl: 60_000 });
export function checkRateLimit(id: string, limit = 10): boolean {
const n = cache.get(id) ?? 0;
if (n >= limit) return false;
cache.set(id, n + 1);
return true;
}
import { z } from 'zod';
const schema = z.object({
messages: z.array(z.object({ role: z.enum(['user', 'assistant']), content: z.string().max(4000) })).max(50),
provider: z.enum(['openai', 'gemini', 'anthropic']).default('gemini'),
});
const parsed = schema.safeParse(await req.json());
if (!parsed.success) return Response.json({ error: 'Invalid request' }, { status: 400 });
NEXT_PUBLIC_maxTokens on every call — prevent runaway costsdangerouslySetInnerHTML AI output — use ReactMarkdownSource: Despoudis, T. — Build AI-Enhanced Web Apps (Packt, 2024)
data-ai
Use when adding AI-powered analytics to a SaaS platform — semantic search over business data, natural language queries, trend detection, anomaly alerts, and AI-generated insights for dashboards. Covers embeddings, NL2SQL, and per-tenant analytics...
data-ai
Design AI-powered analytics dashboards — what metrics to show, how to display AI predictions and confidence, drill-down patterns, KPI cards, trend visualisation, AI Insights panels, export design, and role-based dashboard variants. Invoke when...
development
Use when designing, building, reviewing, or upgrading production software systems that must be secure, performant, maintainable, scalable, and user-centered. Apply before writing specs, code, architecture, APIs, databases, mobile apps, SaaS platforms, or ERP systems.
development
Professional web app UI using commercial templates (Tabler/Bootstrap 5) with strong frontend design direction when needed. Use for CRUD interfaces, dashboards, admin panels with SweetAlert2, DataTables, Flatpickr. Clone seeder-page.php, use...