9.4 KiB
9.4 KiB
Creating APIs
This guide shows how to create new API endpoints.
API Route Structure
API routes live in app/api/ and use file-based routing.
File Structure
app/api/
├── vendors/
│ ├── route.ts # GET /api/vendors, POST /api/vendors
│ └── [id]/
│ └── route.ts # GET /api/vendors/123, DELETE /api/vendors/123
├── auth/
│ ├── logout/route.ts # POST /api/auth/logout
│ └── magic-link/route.ts
Creating a Basic API Route
1. Create the File
// app/api/vendors/route.ts
import { NextResponse } from 'next/server';
import { requireMemberSession } from '@/lib/auth/stytch/server';
export async function GET(request: Request) {
// Check authentication
const session = await getMemberSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Fetch data
const vendors = await vendorRepository.list(session.session_jwt);
// Return response
return NextResponse.json(vendors);
}
2. HTTP Methods
Export functions for each HTTP method:
// GET /api/vendors
export async function GET(request: Request) {
// Handle GET request
}
// POST /api/vendors
export async function POST(request: Request) {
// Handle POST request
}
// PUT /api/vendors
export async function PUT(request: Request) {
// Handle PUT request
}
// DELETE /api/vendors
export async function DELETE(request: Request) {
// Handle DELETE request
}
3. Access the API
Call from frontend:
const response = await fetch('/api/vendors');
const vendors = await response.json();
API Request Flow
graph TD
A[Client makes request] --> B[middleware.ts]
B -->|Public route| C[route.ts handler]
B -->|Protected route| D{Has session?}
D -->|No| E[Return 401]
D -->|Yes| C
C --> F[getMemberSession]
F --> G[Check permissions]
G --> H{Has permission?}
H -->|No| I[Return 403]
H -->|Yes| J[Process request]
J --> K[Return NextResponse]
Authentication in API Routes
Check if Authenticated
import { getMemberSession } from '@/lib/auth/stytch/server';
export async function GET() {
const session = await getMemberSession();
if (!session) {
return NextResponse.json(
{ error: 'Not authenticated' },
{ status: 401 }
);
}
// Authenticated logic
}
Require Authentication
import { requireMemberSession } from '@/lib/auth/stytch/server';
export async function GET() {
// Throws error if not authenticated
const session = await requireMemberSession();
// This code only runs if authenticated
return NextResponse.json({ success: true });
}
Permission Checks in APIs
import { getServerPermissions } from '@/lib/auth/server-permissions';
export async function POST(request: Request) {
const session = await requireMemberSession();
const permissions = await getServerPermissions(session);
if (!permissions.canCreateVendors) {
return NextResponse.json(
{ error: 'Permission denied' },
{ status: 403 }
);
}
// Create vendor logic
}
Reading Request Body
export async function POST(request: Request) {
// Parse JSON body
const body = await request.json();
const { name, email } = body;
// Validate
if (!name || !email) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
// Process request
}
Reading Query Parameters
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const limit = searchParams.get('limit') || '10';
// Use parameters
const results = await repository.list({
page: parseInt(page),
limit: parseInt(limit)
});
return NextResponse.json(results);
}
Dynamic Routes
Create Dynamic Route
// app/api/vendors/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const { id } = params;
const vendor = await vendorRepository.get(id);
if (!vendor) {
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
return NextResponse.json(vendor);
}
Access Dynamic Route
// GET /api/vendors/123
const response = await fetch('/api/vendors/123');
File Uploads
Handle multipart/form-data for file uploads:
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File;
const name = formData.get('name') as string;
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
// Process file
const buffer = await file.arrayBuffer();
// Save or upload file
await saveFile(buffer, file.name);
return NextResponse.json({ success: true });
}
Error Handling
export async function POST(request: Request) {
try {
const body = await request.json();
// Process request
const result = await processData(body);
return NextResponse.json(result);
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Response Formats
Success Response
return NextResponse.json({
success: true,
data: { id: 1, name: 'Vendor' }
});
Error Response
return NextResponse.json(
{
error: 'Validation failed',
details: { name: 'Required' }
},
{ status: 400 }
);
Status Codes
200- OK201- Created400- Bad Request401- Unauthorized403- Forbidden404- Not Found500- Internal Server Error
Using Repositories in APIs
Always use repositories for data access:
import { vendorRepository } from '@/lib/api/api/repositories/vendor-repository';
export async function GET() {
const session = await requireMemberSession();
// Use repository
const vendors = await vendorRepository.list(session.session_jwt);
return NextResponse.json(vendors);
}
CORS Configuration
For external API access, add CORS headers:
export async function GET(request: Request) {
const response = NextResponse.json({ data: 'value' });
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
return response;
}
Webhooks
Handle webhook POST requests:
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get('stripe-signature');
// Verify webhook signature
const event = verifyStripeWebhook(body, signature);
// Process event
if (event.type === 'invoice.paid') {
await handleInvoicePaid(event.data);
}
return NextResponse.json({ received: true });
}
Common Patterns
List Endpoint
export async function GET(request: Request) {
const session = await requireMemberSession();
const items = await repository.list(session.session_jwt);
return NextResponse.json(items);
}
Create Endpoint
export async function POST(request: Request) {
const session = await requireMemberSession();
const permissions = await getServerPermissions(session);
if (!permissions.canCreate) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const item = await repository.create(body, session.session_jwt);
return NextResponse.json(item, { status: 201 });
}
Update Endpoint
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await requireMemberSession();
const body = await request.json();
const updated = await repository.update(
params.id,
body,
session.session_jwt
);
return NextResponse.json(updated);
}
Delete Endpoint
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await requireMemberSession();
const permissions = await getServerPermissions(session);
if (!permissions.canDelete) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
await repository.delete(params.id, session.session_jwt);
return NextResponse.json({ success: true });
}
Testing APIs
With curl
curl http://localhost:3000/api/vendors
curl -X POST http://localhost:3000/api/vendors \
-H "Content-Type: application/json" \
-d '{"name":"Acme Corp"}'
With Postman
- Import the API
- Add authentication cookie
- Test each endpoint
Key Files
app/api/- All API routeslib/api/api/repositories/- Data access layerlib/auth/stytch/server.ts- Auth helpers
Best Practices
- Always check authentication in protected routes
- Re-check permissions - don't trust client
- Use repositories - don't call backend directly
- Validate input before processing
- Handle errors gracefully
- Return proper status codes
- Log errors for debugging
Next Steps
👉 Learn about: Using Hooks