AI SaaS Template Docs
AI SaaS Template Docs
IntroductionQuick StartArchitecture
Vercel DeploymentDocker Deployment
ConfigurationProject StructureDatabase GuideAPI Guide
AuthenticationFile ManagementPayment & Billing
Theme Customization
Developer Guide

API Guide

This guide covers API development patterns, server actions, and webhook implementation in AI SaaS Template.

API Architecture

Next.js App Router API Structure

src/app/api/
├── auth/
│   └── [...all]/
│       └── route.ts          # AI SaaSr Auth handler
└── webhooks/
    └── stripe/
        └── route.ts          # Stripe webhook handler

API Route Pattern

// src/app/api/example/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth/auth';

export async function GET(request: NextRequest) {
  try {
    // Authentication check
    const session = await auth.api.getSession({
      headers: request.headers,
    });

    if (!session?.user) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    // Business logic
    const data = await getExampleData(session.user.id);

    return NextResponse.json({ data });
  } catch (error) {
    console.error('API Error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

export async function POST(request: NextRequest) {
  try {
    const session = await auth.api.getSession({
      headers: request.headers,
    });

    if (!session?.user) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    const body = await request.json();
    
    // Validate input
    const validatedData = validateInput(body);
    
    // Process request
    const result = await processData(validatedData, session.user.id);

    return NextResponse.json(result);
  } catch (error) {
    console.error('API Error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Server Actions

Server Action Pattern

// src/server/actions/example-actions.ts
'use server';

import { auth } from '@/lib/auth/auth';
import { headers } from 'next/headers';
import { getErrorMessage } from './error-messages';
import { ErrorLogger } from '@/lib/debug-logger/logger-utils';

const actionLogger = new ErrorLogger('example-actions');

export interface ExampleResponse {
  success: boolean;
  data?: any;
  error?: string;
}

export async function exampleAction(
  formData: FormData
): Promise<ExampleResponse> {
  let session: { user?: { id: string } } | null = null;

  try {
    // Authentication
    session = await auth.api.getSession({
      headers: await headers(),
    });

    if (!session?.user) {
      throw new Error(await getErrorMessage('unauthorizedAccess'));
    }

    // Extract and validate data
    const data = extractFormData(formData);
    validateData(data);

    // Business logic
    const result = await processAction(data, session.user.id);

    return {
      success: true,
      data: result,
    };
  } catch (error) {
    actionLogger.logError(error as Error, {
      operation: 'exampleAction',
      userId: session?.user?.id,
    });

    return {
      success: false,
      error: error instanceof Error ? error.message : 'Action failed',
    };
  }
}

File Upload Action

// src/server/actions/file-actions.ts
export async function uploadFileAction(
  formData: FormData
): Promise<FileUploadResponse> {
  let session: { user?: User } | null = null;
  let file: File | null = null;

  try {
    session = await auth.api.getSession({
      headers: await headers(),
    });

    if (!session?.user) {
      throw new Error(await getErrorMessage('unauthorizedAccess'));
    }

    file = formData.get('file') as File;

    if (!file) {
      throw new Error(await getErrorMessage('noFileSelected'));
    }

    const fileInfo = await uploadFile(file, session.user.id);

    return {
      success: true,
      file: fileInfo,
    };
  } catch (error) {
    fileErrorLogger.logError(error as Error, {
      operation: 'uploadFile',
      userId: session?.user?.id,
      fileName: file?.name,
    });

    throw new Error(
      error instanceof Error ? error.message : 'File upload failed'
    );
  }
}

Error Handling

// src/server/actions/error-messages.ts
import { getLocale } from 'next-intl/server';

export async function getErrorMessage(key: string): Promise<string> {
  const locale = await getLocale();
  
  const messages = {
    zh: {
      unauthorizedAccess: '未授权访问',
      fileNotFound: '未找到文件',
      fileUploadFailed: '文件上传失败',
      // ... more messages
    },
    en: {
      unauthorizedAccess: 'Unauthorized access',
      fileNotFound: 'File not found',
      fileUploadFailed: 'File upload failed',
      // ... more messages
    },
  };

  const localeMessages = messages[locale as keyof typeof messages] || messages.en;
  return localeMessages[key as keyof typeof localeMessages] || key;
}

Webhook Implementation

Stripe Webhook Handler

// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { StripeProvider } from '@/payment/stripe/provider';
import { paymentRepository } from '@/server/db/repositories/payment-repository';

export async function POST(request: NextRequest) {
  try {
    const body = await request.text();
    const signature = request.headers.get('stripe-signature');

    if (!signature) {
      return NextResponse.json(
        { error: 'Missing signature' },
        { status: 400 }
      );
    }

    const stripeProvider = new StripeProvider();
    
    // Verify webhook signature
    const isValid = await stripeProvider.verifyWebhook(body, signature);
    if (!isValid) {
      return NextResponse.json(
        { error: 'Invalid signature' },
        { status: 400 }
      );
    }

    // Construct event
    const event = stripeProvider.constructWebhookEvent(body, signature);

    // Handle event
    await handleStripeEvent(event);

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json(
      { error: 'Webhook handler failed' },
      { status: 500 }
    );
  }
}

async function handleStripeEvent(event: Stripe.Event) {
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutSessionCompleted(event);
      break;
    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(event);
      break;
    // ... more event handlers
  }
}

Authentication Integration

Protected API Routes

// src/lib/auth/api-middleware.ts
import { auth } from '@/lib/auth/auth';
import { isAdmin } from '@/lib/auth/permissions';
import { NextRequest } from 'next/server';

export async function withAuth(
  request: NextRequest,
  handler: (request: NextRequest, user: any) => Promise<Response>
) {
  try {
    const session = await auth.api.getSession({
      headers: request.headers,
    });

    if (!session?.user) {
      return new Response(
        JSON.stringify({ error: 'Unauthorized' }),
        { status: 401 }
      );
    }

    return handler(request, session.user);
  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Authentication failed' }),
      { status: 401 }
    );
  }
}

export async function withAdminAuth(
  request: NextRequest,
  handler: (request: NextRequest, user: any) => Promise<Response>
) {
  return withAuth(request, async (req, user) => {
    if (!isAdmin(user)) {
      return new Response(
        JSON.stringify({ error: 'Admin access required' }),
        { status: 403 }
      );
    }

    return handler(req, user);
  });
}

API Testing

Integration Tests

// tests/integration/api/example-api.test.ts
import { describe, it, expect } from '@jest/globals';

describe('Example API Integration Tests', () => {
  it('should handle authenticated requests', async () => {
    const mockSession = {
      user: { id: 'user_123', email: '[email protected]' }
    };

    // Mock authentication
    jest.mocked(auth.api.getSession).mockResolvedValue(mockSession);

    const response = await GET(mockRequest);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toHaveProperty('data');
  });

  it('should reject unauthenticated requests', async () => {
    jest.mocked(auth.api.getSession).mockResolvedValue(null);

    const response = await GET(mockRequest);
    const data = await response.json();

    expect(response.status).toBe(401);
    expect(data.error).toBe('Unauthorized');
  });
});

Best Practices

1. Error Handling

  • Always wrap API handlers in try-catch blocks
  • Use consistent error response format
  • Log errors with context information
  • Return appropriate HTTP status codes

2. Authentication

  • Validate sessions on every protected endpoint
  • Use middleware for common authentication logic
  • Implement proper permission checks
  • Handle authentication errors gracefully

3. Input Validation

  • Validate all input data
  • Use TypeScript for type safety
  • Implement rate limiting for public endpoints
  • Sanitize user input

4. Performance

  • Use database connection pooling
  • Implement caching where appropriate
  • Optimize database queries
  • Use streaming for large responses

5. Security

  • Validate webhook signatures
  • Use HTTPS in production
  • Implement CORS properly
  • Never expose sensitive data in responses

Common Patterns

Pagination

export async function getListAction(options: {
  page?: number;
  limit?: number;
  search?: string;
}) {
  const { page = 1, limit = 20, search = '' } = options;
  const offset = (page - 1) * limit;

  const result = await db.select()
    .from(table)
    .where(search ? ilike(table.name, `%${search}%`) : undefined)
    .limit(limit)
    .offset(offset);

  const total = await db.select({ count: count() })
    .from(table)
    .where(search ? ilike(table.name, `%${search}%`) : undefined);

  return {
    data: result,
    pagination: {
      page,
      limit,
      total: total[0]?.count || 0,
      totalPages: Math.ceil((total[0]?.count || 0) / limit),
    },
  };
}

File Upload

export async function handleFileUpload(
  file: File,
  userId: string
): Promise<FileInfo> {
  // Validate file
  const validation = validateFile(file);
  if (!validation.valid) {
    throw new Error(validation.error);
  }

  // Generate unique filename
  const filename = generateUniqueFilename(file.name);
  const r2Key = generateR2Key(filename);

  // Upload to storage
  const uploadResult = await r2Client.upload(r2Key, file);
  
  // Save to database
  const fileRecord = await db.insert(fileTable).values({
    id: generateId(),
    filename,
    originalName: file.name,
    mimeType: file.type,
    size: file.size,
    r2Key,
    uploadUserId: userId,
  }).returning();

  return toFileInfo(fileRecord[0]);
}

This guide provides the foundation for developing robust APIs in AI SaaS Template. Follow these patterns and best practices to maintain consistency and reliability across your API endpoints.

Database Guide

Previous Page

Authentication

Next Page

On this page

API ArchitectureNext.js App Router API StructureAPI Route PatternServer ActionsServer Action PatternFile Upload ActionError HandlingWebhook ImplementationStripe Webhook HandlerAuthentication IntegrationProtected API RoutesAPI TestingIntegration TestsBest Practices1. Error Handling2. Authentication3. Input Validation4. Performance5. SecurityCommon PatternsPaginationFile Upload