# API Development Guide Guide for building and consuming APIs in MetaBuilder. ## API Structure MetaBuilder uses Next.js API routes for backend endpoints: ``` app/api/ ├── users/ │ ├── route.ts # GET /api/users, POST /api/users │ └── [id]/ │ └── route.ts # GET /api/users/[id], PATCH, DELETE ├── workflows/ │ ├── route.ts │ └── [id]/ │ └── route.ts └── auth/ ├── login/route.ts └── logout/route.ts ``` ## Creating an API Endpoint ### 1. Create Route File ```typescript // app/api/posts/route.ts import { NextRequest, NextResponse } from 'next/server' import { validateRequest } from '@/lib/auth' import { prisma } from '@/lib/prisma' /** * GET /api/posts - List user's posts * @requires Level 2+ */ export async function GET(req: NextRequest) { try { // Validate authentication const user = await validateRequest(req) if (!user) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ) } // Get posts for user's tenant const posts = await prisma.post.findMany({ where: { tenantId: user.tenantId, author: { level: { gte: 2 } }, // Level 2+ }, select: { id: true, title: true, content: true, createdAt: true, }, orderBy: { createdAt: 'desc' }, }) return NextResponse.json(posts) } catch (error) { console.error('GET /api/posts error:', error) return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } /** * POST /api/posts - Create new post * @requires Level 2+ * @body {{ title: string, content: string }} */ export async function POST(req: NextRequest) { try { const user = await validateRequest(req) if (!user || user.level < 2) { return NextResponse.json( { error: 'Forbidden' }, { status: 403 } ) } const body = await req.json() // Validate input if (!body.title || !body.content) { return NextResponse.json( { error: 'Title and content required' }, { status: 400 } ) } // Create post const post = await prisma.post.create({ data: { title: body.title, content: body.content, authorId: user.id, tenantId: user.tenantId, }, }) return NextResponse.json(post, { status: 201 }) } catch (error) { console.error('POST /api/posts error:', error) return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } ``` ### 2. Create Dynamic Route ```typescript // app/api/posts/[id]/route.ts import { NextRequest, NextResponse } from 'next/server' import { validateRequest, canAccessLevel } from '@/lib/auth' import { prisma } from '@/lib/prisma' interface RouteParams { params: { id: string } } /** * GET /api/posts/[id] - Get specific post */ export async function GET( req: NextRequest, { params }: RouteParams ) { try { const user = await validateRequest(req) if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const post = await prisma.post.findFirst({ where: { id: params.id, tenantId: user.tenantId, }, include: { author: true }, }) if (!post) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } return NextResponse.json(post) } catch (error) { return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } /** * PATCH /api/posts/[id] - Update post * @requires Owner or Level 3+ */ export async function PATCH( req: NextRequest, { params }: RouteParams ) { try { const user = await validateRequest(req) if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } // Check ownership or admin const post = await prisma.post.findFirst({ where: { id: params.id, tenantId: user.tenantId }, }) if (!post) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } const isOwner = post.authorId === user.id const isAdmin = canAccessLevel(user.level, 3) if (!isOwner && !isAdmin) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } const body = await req.json() const updated = await prisma.post.update({ where: { id: params.id }, data: { title: body.title, content: body.content, }, }) return NextResponse.json(updated) } catch (error) { return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } /** * DELETE /api/posts/[id] - Delete post * @requires Owner or Level 3+ */ export async function DELETE( req: NextRequest, { params }: RouteParams ) { try { const user = await validateRequest(req) if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const post = await prisma.post.findFirst({ where: { id: params.id, tenantId: user.tenantId }, }) if (!post) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } if (post.authorId !== user.id && !canAccessLevel(user.level, 3)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } await prisma.post.delete({ where: { id: params.id } }) return NextResponse.json({ success: true }) } catch (error) { return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ) } } ``` ## Consuming APIs ### From React Components ```typescript import { useEffect, useState } from 'react' export const PostList = () => { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { // Fetch posts const fetchPosts = async () => { try { const response = await fetch('/api/posts') if (!response.ok) { throw new Error(`API error: ${response.status}`) } const data = await response.json() setPosts(data) } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } finally { setLoading(false) } } fetchPosts() }, []) if (loading) return
Loading...
if (error) return
Error: {error}
return ( ) } ``` ### Using Custom Hook ```typescript // hooks/useFetch.ts import { useState, useEffect } from 'react' export const useFetch = (url: string) => { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { const fetchData = async () => { try { const response = await fetch(url) if (!response.ok) throw new Error('API error') const json = await response.json() setData(json) } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error')) } finally { setLoading(false) } } fetchData() }, [url]) return { data, loading, error } } // Usage const { data: posts } = useFetch('/api/posts') ``` ## Error Handling ### Common Status Codes | Code | Meaning | Handling | |------|---------|----------| | 200 | Success | Process data | | 201 | Created | Show success message | | 400 | Bad Request | Validate input | | 401 | Unauthorized | Redirect to login | | 403 | Forbidden | Show permission error | | 404 | Not Found | Show not found message | | 500 | Server Error | Show error, log issue | ### Consistent Error Response ```typescript export interface ApiError { error: string code?: string details?: Record } // Return error return NextResponse.json( { error: 'Validation failed', code: 'INVALID_INPUT', details: { title: 'Title is required', email: 'Email format invalid', }, }, { status: 400 } ) ``` ## Authentication & Authorization ### Validate Request ```typescript import { validateRequest } from '@/lib/auth' const user = await validateRequest(req) if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } ``` ### Check Permission Level ```typescript import { canAccessLevel } from '@/lib/auth' if (!canAccessLevel(user.level, 3)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } ``` ## Rate Limiting ```typescript // Simple in-memory rate limiter const rateLimitMap = new Map() function checkRateLimit(userId: string, limit: number = 100): boolean { const now = Date.now() const oneMinuteAgo = now - 60 * 1000 const timestamps = rateLimitMap.get(userId) || [] const recent = timestamps.filter(t => t > oneMinuteAgo) if (recent.length >= limit) { return false } recent.push(now) rateLimitMap.set(userId, recent) return true } ``` ## Testing APIs ```typescript // __tests__/api/posts.spec.ts import { GET, POST } from '@/app/api/posts/route' describe('POST API', () => { it('should get posts', async () => { const req = new Request('http://localhost/api/posts') const response = await GET(req as any) expect(response.status).toBe(200) }) }) ``` ## Best Practices ✅ **Do:** - Always validate authentication - Check permissions on sensitive operations - Filter data by tenantId - Use TypeScript for request/response types - Document endpoints with JSDoc - Handle errors consistently - Validate input before processing - Use appropriate HTTP status codes ❌ **Don't:** - Trust user input without validation - Forget tenantId in queries - Return sensitive data in error messages - Mix multiple resources in one endpoint - Skip error handling - Use GET for data modification - Expose database errors to client See related guides in `/docs/` for more patterns.