mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
13 KiB
13 KiB
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
├── register/route.ts
├── session/route.ts
└── logout/route.ts
Users API (DBAL-backed)
The users endpoints run on the server and use DBAL helpers + credential storage.
Create User
POST /api/users
Content-Type: application/json
{
"username": "newuser",
"email": "newuser@example.com",
"role": "admin",
"password": "s3cret",
"bio": "Short bio",
"profilePicture": "https://example.com/avatar.png"
}
Response:
{
"user": {
"id": "user_123",
"username": "newuser",
"email": "newuser@example.com",
"role": "admin",
"bio": "Short bio",
"profilePicture": "https://example.com/avatar.png",
"createdAt": 1715612345678
}
}
Update User
PATCH /api/users/{id}
Content-Type: application/json
{
"email": "updated@example.com",
"password": "new-secret"
}
Delete User
DELETE /api/users/{id}
Auth API (Session-backed)
Authentication uses a database-backed session with an httpOnly cookie (mb_session).
Sessions default to a 7 day TTL and are refreshed on /api/auth/session.
Notes:
- Seeded accounts set
firstLogin=trueto force an immediate password change on first sign-in. - Updating credentials clears
firstLoginand updatespasswordChangeTimestampon the user.
Login
POST /api/auth/login
Content-Type: application/json
{
"identifier": "alice@example.com",
"password": "s3cret"
}
Response:
{
"user": {
"id": "user_123",
"username": "alice",
"email": "alice@example.com",
"role": "user",
"createdAt": 1715612345678
}
}
Register
POST /api/auth/register
Content-Type: application/json
{
"username": "newuser",
"email": "newuser@example.com",
"password": "s3cret"
}
Session Check
GET /api/auth/session
Response:
{
"user": null
}
Logout
POST /api/auth/logout
Screenshot Analysis API
Local heuristic analysis for the Screenshot Analyzer UI.
POST /api/screenshot/analyze
Content-Type: application/json
{
"title": "Dashboard",
"url": "https://example.com/dashboard",
"viewport": { "width": 1280, "height": 720 },
"textSample": "First 3k characters of text",
"htmlSample": "First 6k characters of HTML"
}
Response:
{
"report": "Screenshot Analysis (local heuristics)...",
"metrics": {
"wordCount": 120,
"headingCount": 4,
"h1Count": 1,
"h2Count": 2,
"h3Count": 1,
"imgCount": 3,
"imgMissingAltCount": 1,
"linkCount": 12,
"buttonCount": 4,
"formCount": 1,
"inputCount": 3
},
"warnings": [
"Images missing alt text: 1."
]
}
Creating an API Endpoint
1. Create Route File
// 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
// 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
import { useEffect, useState } from 'react'
export const PostList = () => {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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 <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Using Custom Hook
// hooks/useFetch.ts
import { useState, useEffect } from 'react'
export const useFetch = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(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
export interface ApiError {
error: string
code?: string
details?: Record<string, string>
}
// 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
import { validateRequest } from '@/lib/auth'
const user = await validateRequest(req)
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
Check Permission Level
import { canAccessLevel } from '@/lib/auth'
if (!canAccessLevel(user.level, 3)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
Rate Limiting
// Simple in-memory rate limiter
const rateLimitMap = new Map<string, number[]>()
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
// __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.