Files
ColaFlow/docs/frontend/api-integration-guide.md
Yaojia Wang fe8ad1c1f9
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
In progress
2025-11-03 11:51:02 +01:00

27 KiB

API Integration Guide - Frontend

Document Overview

This guide provides complete documentation for integrating ColaFlow's frontend with the .NET 9 backend API, including all endpoints, request/response formats, error handling, and best practices.

Base URL: http://localhost:5000/api (Development) Production URL: https://api.colaflow.com/api


Table of Contents

  1. API Client Configuration
  2. Authentication APIs
  3. Tenant Management APIs
  4. MCP Token APIs
  5. Error Handling
  6. Rate Limiting
  7. Testing with Mock Data

API Client Configuration

Axios Instance Setup

File: lib/api-client.ts

import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
import { useAuthStore } from '@/stores/useAuthStore';

const apiClient: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api',
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true, // Important: Send httpOnly cookies
});

// Request Interceptor: Add Authorization header
apiClient.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const { accessToken, tenant } = useAuthStore.getState();

    // Add Bearer token
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }

    // Add tenant ID header (optional, for non-browser clients)
    if (tenant && !config.headers['X-Tenant-Id']) {
      config.headers['X-Tenant-Id'] = tenant.id;
    }

    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response Interceptor: Handle token refresh
let isRefreshing = false;
let failedQueue: Array<{
  resolve: (value?: unknown) => void;
  reject: (reason?: unknown) => void;
}> = [];

const processQueue = (error: AxiosError | null, token: string | null = null) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });

  failedQueue = [];
};

apiClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };

    // If 401 error and not already retrying
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // If refresh is in progress, queue this request
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
          .then(() => {
            return apiClient(originalRequest);
          })
          .catch((err) => {
            return Promise.reject(err);
          });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        // Call refresh token endpoint
        const response = await axios.post(
          `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`,
          {},
          { withCredentials: true }
        );

        const { accessToken } = response.data;

        // Update token in store
        useAuthStore.getState().setAccessToken(accessToken);

        // Update Authorization header
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;

        // Process queued requests
        processQueue(null, accessToken);

        // Retry original request
        return apiClient(originalRequest);
      } catch (refreshError) {
        // Refresh failed, logout user
        processQueue(refreshError as AxiosError, null);
        useAuthStore.getState().clearAuth();

        // Redirect to login
        if (typeof window !== 'undefined') {
          window.location.href = '/login';
        }

        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

export default apiClient;

Authentication APIs

1. Local Login

Endpoint: POST /api/auth/login

Request:

interface LoginRequest {
  email: string;
  password: string;
  rememberMe?: boolean;
}

Example:

{
  "email": "admin@acme.com",
  "password": "SecurePassword123!",
  "rememberMe": true
}

Response (200 OK):

interface LoginResponse {
  user: {
    id: string;
    email: string;
    fullName: string;
    role: 'Admin' | 'Member' | 'Viewer';
    authProvider: 'Local' | 'AzureAD' | 'Google' | 'Okta';
    avatarUrl?: string;
  };
  tenant: {
    id: string;
    slug: string;
    name: string;
    plan: 'Free' | 'Starter' | 'Professional' | 'Enterprise';
    status: 'Active' | 'Trial' | 'Suspended' | 'Cancelled';
    ssoEnabled: boolean;
  };
  accessToken: string;
  // Refresh token is set as httpOnly cookie automatically
}

Example:

{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "admin@acme.com",
    "fullName": "John Doe",
    "role": "Admin",
    "authProvider": "Local",
    "avatarUrl": null
  },
  "tenant": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "slug": "acme",
    "name": "Acme Corporation",
    "plan": "Professional",
    "status": "Active",
    "ssoEnabled": true
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Error Responses:

  • 400 Bad Request: Invalid input (email/password missing)
  • 401 Unauthorized: Invalid credentials
  • 403 Forbidden: Account suspended or disabled
  • 404 Not Found: Tenant not found

2. SSO Login Initiation

Endpoint: GET /api/auth/sso/login?provider={provider}&redirect={redirect}

Query Parameters:

  • provider (required): azuread | google | okta | saml
  • redirect (optional): URL to redirect after successful login (default: /)

Example:

GET /api/auth/sso/login?provider=azuread&redirect=/dashboard

Response (302 Redirect): Redirects to IdP login page with state parameter.

Frontend Implementation:

// services/auth.service.ts
export const authService = {
  loginWithSso: (provider: 'azuread' | 'google' | 'okta' | 'saml', redirectUrl?: string) => {
    const params = new URLSearchParams({ provider });
    if (redirectUrl) params.append('redirect', redirectUrl);

    // Backend will redirect, so we navigate directly
    window.location.href = `/api/auth/sso/login?${params.toString()}`;
  },
};

3. SSO Callback

Endpoint: GET /api/auth/sso/callback?code={code}&state={state}

Query Parameters:

  • code (required): Authorization code from IdP
  • state (required): CSRF protection state

Response (302 Redirect): Redirects to frontend callback page with token:

https://app.colaflow.com/auth/callback?token={accessToken}&tenant={tenantSlug}

Frontend Callback Handler:

File: app/(auth)/auth/callback/page.tsx

'use client';

import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAuthStore } from '@/stores/useAuthStore';

export default function SsoCallbackPage() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [error, setError] = useState<string | null>(null);
  const setUser = useAuthStore((state) => state.setUser);

  useEffect(() => {
    const handleCallback = async () => {
      try {
        // Get token from URL
        const token = searchParams.get('token');
        const tenantSlug = searchParams.get('tenant');

        if (!token || !tenantSlug) {
          throw new Error('Missing token or tenant in callback');
        }

        // Validate state parameter (if frontend initiated SSO)
        const state = searchParams.get('state');
        const storedState = sessionStorage.getItem('sso_state');

        if (storedState && state !== storedState) {
          throw new Error('Invalid state parameter (CSRF protection)');
        }

        // Clear stored state
        sessionStorage.removeItem('sso_state');

        // Decode JWT to get user info (or call /api/auth/me)
        // For now, we'll call /api/auth/me with the token
        const response = await fetch('/api/auth/me', {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        });

        if (!response.ok) {
          throw new Error('Failed to fetch user info');
        }

        const data = await response.json();

        // Store in AuthStore
        setUser(data.user, data.tenant, token);

        // Redirect to original page or dashboard
        const redirectUrl = sessionStorage.getItem('redirect_after_login') || '/dashboard';
        sessionStorage.removeItem('redirect_after_login');

        router.push(redirectUrl);
      } catch (err: any) {
        console.error('SSO callback error:', err);
        setError(err.message || 'SSO authentication failed');
      }
    };

    handleCallback();
  }, [searchParams, router, setUser]);

  if (error) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md">
          <h2 className="text-lg font-semibold text-red-800 mb-2">
            Authentication Failed
          </h2>
          <p className="text-red-700 mb-4">{error}</p>
          <button
            onClick={() => router.push('/login')}
            className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
          >
            Back to Login
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
        <p className="text-gray-600">Completing sign-in...</p>
      </div>
    </div>
  );
}

4. Logout

Endpoint: POST /api/auth/logout

Request: Empty body

Response (200 OK):

{
  "message": "Logged out successfully"
}

Frontend Implementation:

// hooks/auth/useLogout.ts
import { useMutation } from '@tanstack/react-query';
import { authService } from '@/services/auth.service';
import { useAuthStore } from '@/stores/useAuthStore';
import { useRouter } from 'next/navigation';

export function useLogout() {
  const clearAuth = useAuthStore((state) => state.clearAuth);
  const router = useRouter();

  return useMutation({
    mutationFn: authService.logout,
    onSuccess: () => {
      clearAuth();
      router.push('/login');
    },
  });
}

5. Refresh Token

Endpoint: POST /api/auth/refresh

Request: Empty body (uses httpOnly cookie)

Response (200 OK):

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Error Responses:

  • 401 Unauthorized: Refresh token expired or invalid

Frontend Implementation:

Handled automatically by Axios interceptor (see API Client Configuration)


6. Get Current User

Endpoint: GET /api/auth/me

Headers:

Authorization: Bearer {accessToken}

Response (200 OK):

{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "admin@acme.com",
    "fullName": "John Doe",
    "role": "Admin",
    "authProvider": "AzureAD",
    "avatarUrl": "https://graph.microsoft.com/v1.0/me/photo/$value"
  },
  "tenant": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "slug": "acme",
    "name": "Acme Corporation",
    "plan": "Professional",
    "status": "Active",
    "ssoEnabled": true
  }
}

Tenant Management APIs

1. Register Tenant (Signup)

Endpoint: POST /api/tenants/register

Request:

interface RegisterTenantRequest {
  // Organization info
  organizationName: string;
  slug: string;

  // Admin user info
  adminEmail: string;
  adminPassword: string;
  adminFullName: string;

  // Subscription
  plan: 'Free' | 'Starter' | 'Professional' | 'Enterprise';
}

Example:

{
  "organizationName": "Acme Corporation",
  "slug": "acme",
  "adminEmail": "admin@acme.com",
  "adminPassword": "SecurePassword123!",
  "adminFullName": "John Doe",
  "plan": "Professional"
}

Response (201 Created):

{
  "tenant": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "slug": "acme",
    "name": "Acme Corporation",
    "plan": "Professional",
    "status": "Active"
  },
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "admin@acme.com",
    "fullName": "John Doe",
    "role": "Admin"
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Error Responses:

  • 400 Bad Request: Validation errors (slug format, password strength)
  • 409 Conflict: Slug already taken

2. Check Slug Availability

Endpoint: GET /api/tenants/check-slug?slug={slug}

Query Parameters:

  • slug (required): Tenant slug to check

Example:

GET /api/tenants/check-slug?slug=acme

Response (200 OK):

{
  "slug": "acme",
  "available": false
}

Frontend Implementation (with debounce):

// hooks/tenants/useCheckSlug.ts
import { useQuery } from '@tanstack/react-query';
import { tenantService } from '@/services/tenant.service';
import { useMemo } from 'react';
import debounce from 'lodash-es/debounce';

export function useCheckSlug(slug: string) {
  // Debounce slug changes
  const debouncedSlug = useMemo(
    () => debounce((value: string) => value, 500),
    []
  );

  const trimmedSlug = slug.trim().toLowerCase();

  return useQuery({
    queryKey: ['check-slug', trimmedSlug],
    queryFn: () => tenantService.checkSlugAvailability(trimmedSlug),
    enabled: trimmedSlug.length >= 3, // Only check if at least 3 chars
    staleTime: 5000, // Cache for 5 seconds
  });
}

3. Get SSO Configuration

Endpoint: GET /api/tenants/{tenantId}/sso

Headers:

Authorization: Bearer {accessToken}

Response (200 OK):

{
  "enabled": true,
  "provider": "AzureAD",
  "authority": "https://login.microsoftonline.com/tenant-id",
  "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "metadataUrl": "https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration",
  "autoProvisionUsers": true,
  "allowedDomains": ["acme.com", "acme.org"]
}

Note: clientSecret is never returned in API responses.


4. Update SSO Configuration

Endpoint: PUT /api/tenants/{tenantId}/sso

Headers:

Authorization: Bearer {accessToken}

Request:

interface UpdateSsoConfigRequest {
  provider: 'AzureAD' | 'Google' | 'Okta' | 'GenericSaml';

  // OIDC fields (for AzureAD, Google, Okta)
  authority?: string;
  clientId?: string;
  clientSecret?: string;
  metadataUrl?: string;

  // SAML fields (for GenericSaml)
  entityId?: string;
  signOnUrl?: string;
  certificate?: string;

  // Common fields
  autoProvisionUsers?: boolean;
  allowedDomains?: string[];
}

Example (Azure AD):

{
  "provider": "AzureAD",
  "authority": "https://login.microsoftonline.com/tenant-id",
  "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "clientSecret": "client-secret-here",
  "metadataUrl": "https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration",
  "autoProvisionUsers": true,
  "allowedDomains": ["acme.com"]
}

Response (200 OK):

{
  "message": "SSO configuration updated successfully"
}

Error Responses:

  • 400 Bad Request: Validation errors (missing fields, invalid URLs)
  • 403 Forbidden: Only Admin users can update SSO config
  • 422 Unprocessable Entity: SSO not available for Free plan

5. Test SSO Connection

Endpoint: POST /api/tenants/{tenantId}/sso/test

Headers:

Authorization: Bearer {accessToken}

Response (200 OK):

{
  "success": true,
  "message": "SSO connection successful"
}

Response (200 OK) - Failed Test:

{
  "success": false,
  "message": "Failed to connect to IdP: Connection timeout"
}

MCP Token APIs

1. List MCP Tokens

Endpoint: GET /api/mcp/tokens

Headers:

Authorization: Bearer {accessToken}

Response (200 OK):

{
  "tokens": [
    {
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "name": "Claude AI Agent",
      "permissions": {
        "projects": ["read", "search"],
        "issues": ["read", "create", "update", "search"],
        "documents": ["read", "search"]
      },
      "status": "Active",
      "createdAt": "2025-11-01T10:00:00Z",
      "lastUsedAt": "2025-11-03T14:30:00Z",
      "expiresAt": "2026-11-01T10:00:00Z",
      "usageCount": 1234
    }
  ]
}

2. Create MCP Token

Endpoint: POST /api/mcp/tokens

Headers:

Authorization: Bearer {accessToken}

Request:

interface CreateMcpTokenRequest {
  name: string;
  permissions: {
    [resource: string]: string[];
  };
  expiresAt?: string; // ISO 8601 date
  ipWhitelist?: string[];
}

Example:

{
  "name": "Claude AI Agent",
  "permissions": {
    "projects": ["read", "search"],
    "issues": ["read", "create", "update", "search"],
    "documents": ["read", "create", "search"],
    "reports": ["read"]
  },
  "expiresAt": "2026-11-01T00:00:00Z",
  "ipWhitelist": ["192.168.1.100", "10.0.0.0/24"]
}

Response (201 Created):

{
  "tokenId": "770e8400-e29b-41d4-a716-446655440002",
  "token": "mcp_acme_7f3d8a9c4e1b2f5a6d8c9e0f1a2b3c4d",
  "name": "Claude AI Agent",
  "createdAt": "2025-11-03T10:00:00Z",
  "expiresAt": "2026-11-01T00:00:00Z"
}

IMPORTANT: The token field is shown only once. It will never be returned again.

Error Responses:

  • 400 Bad Request: Invalid permissions or validation errors
  • 403 Forbidden: Only Admin users can create MCP tokens
  • 422 Unprocessable Entity: Invalid resource or operation names

3. Get MCP Token Details

Endpoint: GET /api/mcp/tokens/{tokenId}

Headers:

Authorization: Bearer {accessToken}

Response (200 OK):

{
  "id": "770e8400-e29b-41d4-a716-446655440002",
  "name": "Claude AI Agent",
  "permissions": {
    "projects": ["read", "search"],
    "issues": ["read", "create", "update", "search"]
  },
  "status": "Active",
  "createdAt": "2025-11-01T10:00:00Z",
  "lastUsedAt": "2025-11-03T14:30:00Z",
  "expiresAt": "2026-11-01T10:00:00Z",
  "usageCount": 1234,
  "ipWhitelist": ["192.168.1.100"]
}

4. Revoke MCP Token

Endpoint: DELETE /api/mcp/tokens/{tokenId}

Headers:

Authorization: Bearer {accessToken}

Request Body (Optional):

{
  "reason": "No longer needed"
}

Response (200 OK):

{
  "message": "Token revoked successfully"
}

5. Get MCP Token Audit Logs

Endpoint: GET /api/mcp/tokens/{tokenId}/audit-logs

Headers:

Authorization: Bearer {accessToken}

Query Parameters:

  • page (optional): Page number (default: 1)
  • limit (optional): Results per page (default: 50, max: 100)
  • startDate (optional): Filter by date range (ISO 8601)
  • endDate (optional): Filter by date range (ISO 8601)
  • statusCode (optional): Filter by HTTP status code (e.g., 200, 401, 403)

Example:

GET /api/mcp/tokens/{tokenId}/audit-logs?page=1&limit=50&statusCode=200

Response (200 OK):

{
  "logs": [
    {
      "id": "880e8400-e29b-41d4-a716-446655440003",
      "timestamp": "2025-11-03T14:30:15Z",
      "httpMethod": "GET",
      "endpoint": "/api/mcp/issues/search",
      "statusCode": 200,
      "durationMs": 125,
      "ipAddress": "192.168.1.100",
      "userAgent": "Claude-MCP-Client/1.0"
    },
    {
      "id": "880e8400-e29b-41d4-a716-446655440004",
      "timestamp": "2025-11-03T14:28:42Z",
      "httpMethod": "POST",
      "endpoint": "/api/mcp/issues",
      "statusCode": 201,
      "durationMs": 342,
      "ipAddress": "192.168.1.100",
      "userAgent": "Claude-MCP-Client/1.0"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "totalPages": 5,
    "totalCount": 234
  }
}

Error Handling

Standard Error Response Format

interface ApiError {
  error: string;
  message: string;
  statusCode: number;
  timestamp: string;
  details?: Record<string, string[]>; // Validation errors
}

Example (400 Bad Request):

{
  "error": "Validation Error",
  "message": "Invalid input data",
  "statusCode": 400,
  "timestamp": "2025-11-03T10:00:00Z",
  "details": {
    "email": ["Email is required", "Invalid email format"],
    "password": ["Password must be at least 8 characters"]
  }
}

HTTP Status Codes

Code Meaning Example
200 Success Login successful
201 Created Tenant registered
204 No Content Token revoked
400 Bad Request Invalid input data
401 Unauthorized Invalid credentials or expired token
403 Forbidden Insufficient permissions
404 Not Found Tenant not found
409 Conflict Slug already taken
422 Unprocessable Entity Business logic error (e.g., SSO not available for Free plan)
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server error

Frontend Error Handling

Global Error Handler:

// lib/api-client.ts (add to interceptor)
apiClient.interceptors.response.use(
  (response) => response,
  (error: AxiosError<ApiError>) => {
    // Handle different error types
    if (error.response) {
      const { status, data } = error.response;

      switch (status) {
        case 400:
          // Show validation errors
          if (data.details) {
            Object.entries(data.details).forEach(([field, errors]) => {
              errors.forEach((err) => {
                toast.error(`${field}: ${err}`);
              });
            });
          } else {
            toast.error(data.message || 'Invalid input');
          }
          break;

        case 401:
          // Handled by token refresh interceptor
          break;

        case 403:
          toast.error('You do not have permission to perform this action');
          break;

        case 404:
          toast.error('Resource not found');
          break;

        case 409:
          toast.error(data.message || 'Conflict: Resource already exists');
          break;

        case 422:
          toast.error(data.message || 'Business logic error');
          break;

        case 429:
          toast.error('Too many requests. Please try again later.');
          break;

        case 500:
          toast.error('Server error. Please try again later.');
          console.error('Server error:', data);
          break;

        default:
          toast.error('An unexpected error occurred');
      }
    } else if (error.request) {
      // Network error (no response)
      toast.error('Network error. Please check your connection.');
    } else {
      // Other errors
      toast.error('An unexpected error occurred');
    }

    return Promise.reject(error);
  }
);

Rate Limiting

Rate Limit Headers

Backend includes rate limit information in response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1699000000

Frontend Rate Limit Handling

// lib/api-client.ts
apiClient.interceptors.response.use(
  (response) => {
    // Check rate limit headers
    const limit = response.headers['x-ratelimit-limit'];
    const remaining = response.headers['x-ratelimit-remaining'];
    const reset = response.headers['x-ratelimit-reset'];

    if (remaining && parseInt(remaining) < 10) {
      console.warn(`Rate limit warning: ${remaining}/${limit} requests remaining`);
    }

    return response;
  },
  (error) => {
    if (error.response?.status === 429) {
      const reset = error.response.headers['x-ratelimit-reset'];
      const resetDate = new Date(parseInt(reset) * 1000);

      toast.error(`Rate limit exceeded. Try again at ${resetDate.toLocaleTimeString()}`);
    }

    return Promise.reject(error);
  }
);

Testing with Mock Data

MSW (Mock Service Worker) Setup

File: mocks/handlers.ts

import { http, HttpResponse } from 'msw';

export const handlers = [
  // Auth: Login
  http.post('/api/auth/login', async ({ request }) => {
    const body = await request.json();

    if (body.email === 'admin@acme.com' && body.password === 'password') {
      return HttpResponse.json({
        user: {
          id: '550e8400-e29b-41d4-a716-446655440000',
          email: 'admin@acme.com',
          fullName: 'John Doe',
          role: 'Admin',
          authProvider: 'Local',
        },
        tenant: {
          id: '660e8400-e29b-41d4-a716-446655440001',
          slug: 'acme',
          name: 'Acme Corporation',
          plan: 'Professional',
          status: 'Active',
          ssoEnabled: true,
        },
        accessToken: 'mock-access-token',
      });
    }

    return HttpResponse.json(
      { error: 'Invalid credentials', message: 'Email or password is incorrect' },
      { status: 401 }
    );
  }),

  // Tenants: Check Slug
  http.get('/api/tenants/check-slug', ({ request }) => {
    const url = new URL(request.url);
    const slug = url.searchParams.get('slug');

    const takenSlugs = ['acme', 'beta', 'test'];

    return HttpResponse.json({
      slug,
      available: !takenSlugs.includes(slug || ''),
    });
  }),

  // MCP: List Tokens
  http.get('/api/mcp/tokens', () => {
    return HttpResponse.json({
      tokens: [
        {
          id: '770e8400-e29b-41d4-a716-446655440002',
          name: 'Claude AI Agent',
          permissions: {
            projects: ['read', 'search'],
            issues: ['read', 'create', 'update', 'search'],
          },
          status: 'Active',
          createdAt: '2025-11-01T10:00:00Z',
          lastUsedAt: '2025-11-03T14:30:00Z',
          expiresAt: '2026-11-01T10:00:00Z',
          usageCount: 1234,
        },
      ],
    });
  }),

  // MCP: Create Token
  http.post('/api/mcp/tokens', async ({ request }) => {
    const body = await request.json();

    return HttpResponse.json(
      {
        tokenId: '770e8400-e29b-41d4-a716-446655440002',
        token: 'mcp_acme_7f3d8a9c4e1b2f5a6d8c9e0f1a2b3c4d',
        name: body.name,
        createdAt: new Date().toISOString(),
        expiresAt: body.expiresAt,
      },
      { status: 201 }
    );
  }),
];

Setup MSW:

// mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

Enable MSW in Development:

// app/layout.tsx
if (process.env.NODE_ENV === 'development') {
  import('../mocks/browser').then(({ worker }) => {
    worker.start();
  });
}

Conclusion

This API integration guide provides all the information needed to integrate ColaFlow's frontend with the .NET 9 backend API. All endpoints are documented with complete request/response examples, error handling, and best practices.

Key Takeaways:

  • Use Axios interceptors for automatic token injection and refresh
  • Handle errors globally with consistent UI feedback
  • Use TanStack Query for data fetching and caching
  • Test with MSW before backend is ready
  • Monitor rate limits to avoid 429 errors

Next Document: state-management-guide.md (Zustand + TanStack Query integration)