| name | api-integration-enforcer |
| description | Enforces proper API v1 integration patterns for Token Dashboard backend communication. Ensures correct authentication headers, tenant/project headers, error handling, and response parsing. Use when creating or modifying service layer code, API calls, or debugging API integration issues. |
API v1 Integration Patterns
Purpose
Enforces consistent and correct API integration patterns for Token Dashboard's backend communication. Ensures proper authentication, multi-tenancy headers, error handling, and response parsing.
When to Use This Skill
- Creating new service files in
src/services/
- Modifying existing API integration code
- Debugging API request/response issues
- Implementing new API endpoints
- Adding authentication to requests
- Handling multi-tenant API calls
Quick Reference
API v1 Base Configuration
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';
const API_PREFIX = '/api/v1';
Required Headers
All authenticated requests MUST include:
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'X-Tenant-ID': tenantId,
'X-Project-ID': projectId
};
HTTP Wrapper Pattern
Centralized HTTP Service
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';
const API_PREFIX = '/api/v1';
export async function apiRequest(endpoint, options = {}, token, tenantId, projectId = null) {
const url = `${BASE_URL}${API_PREFIX}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (tenantId) {
headers['X-Tenant-ID'] = tenantId;
}
if (projectId) {
headers['X-Project-ID'] = projectId;
}
const config = {
...options,
headers
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({
message: `HTTP ${response.status}: ${response.statusText}`
}));
throw new Error(error.message || `Request failed with status ${response.status}`);
}
if (response.status === 204) {
return null;
}
return await response.json();
} catch (error) {
console.error(`API request failed: ${endpoint}`, error);
throw error;
}
}
export function get(endpoint, token, tenantId, projectId, params = {}) {
const queryString = new URLSearchParams(params).toString();
const fullEndpoint = queryString ? `${endpoint}?${queryString}` : endpoint;
return apiRequest(fullEndpoint, { method: 'GET' }, token, tenantId, projectId);
}
export function post(endpoint, data, token, tenantId, projectId) {
return apiRequest(
endpoint,
{
method: 'POST',
body: JSON.stringify(data)
},
token,
tenantId,
projectId
);
}
export function put(endpoint, data, token, tenantId, projectId) {
return apiRequest(
endpoint,
{
method: 'PUT',
body: JSON.stringify(data)
},
token,
tenantId,
projectId
);
}
export function del(endpoint, token, tenantId, projectId) {
return apiRequest(
endpoint,
{ method: 'DELETE' },
token,
tenantId,
projectId
);
}
Service Layer Patterns
Token Service Example
import { get, post, put, del } from './http';
export const tokenService = {
async list(token, tenantId, projectId, options = {}) {
const { page = 1, limit = 10, category, search } = options;
const params = { page, limit };
if (category) params.category = category;
if (search) params.search = search;
return await get(
`/tenants/${tenantId}/projects/${projectId}/tokens`,
token,
tenantId,
projectId,
params
);
},
async get(token, tenantId, projectId, tokenId) {
return await get(
`/tenants/${tenantId}/projects/${projectId}/tokens/${tokenId}`,
token,
tenantId,
projectId
);
},
async create(token, tenantId, projectId, tokenData) {
return await post(
`/tenants/${tenantId}/projects/${projectId}/tokens`,
tokenData,
token,
tenantId,
projectId
);
},
async update(token, tenantId, projectId, tokenId, updates) {
return await put(
`/tenants/${tenantId}/projects/${projectId}/tokens/${tokenId}`,
updates,
token,
tenantId,
projectId
);
},
async remove(token, tenantId, projectId, tokenId) {
return await del(
`/tenants/${tenantId}/projects/${projectId}/tokens/${tokenId}`,
token,
tenantId,
projectId
);
},
async import(token, tenantId, projectId, tokens) {
return await post(
`/tenants/${tenantId}/projects/${projectId}/tokens/import`,
tokens,
token,
tenantId,
projectId
);
},
getCategories() {
return [
'color',
'typography',
'spacing',
'shadow',
'border',
'radius',
'opacity',
'z-index',
'timing'
];
}
};
API Keys Service Example
import { get, post, del } from './http';
export const apiKeyService = {
async list(token, projectId) {
const response = await get(
`/projects/${projectId}/keys`,
token,
null,
projectId
);
return response.keys || response.items || response;
},
async create(token, projectId, keyData) {
const response = await post(
`/projects/${projectId}/keys`,
keyData,
token,
null,
projectId
);
return response.apiKey || response.key || response;
},
async rotate(token, projectId, keyId) {
const response = await post(
`/projects/${projectId}/keys/${keyId}/rotate`,
{},
token,
null,
projectId
);
return response.apiKey || response.key || response;
},
async revoke(token, projectId, keyId) {
return await del(
`/projects/${projectId}/keys/${keyId}`,
token,
null,
projectId
);
}
};
Membership Service Example
import { get } from './http';
export const membershipService = {
async getMyMemberships(token) {
const response = await get(
'/users/me/memberships',
token,
null,
null
);
return response.memberships || response;
}
};
Error Handling Patterns
Service-Level Error Handling
try {
const result = await tokenService.create(token, tenantId, projectId, tokenData);
return result;
} catch (error) {
console.error('Failed to create token:', error);
throw error;
}
Component-Level Error Handling
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleCreate = async (tokenData) => {
try {
setLoading(true);
setError(null);
const result = await tokenService.create(token, tenantId, projectId, tokenData);
onSuccess(result);
} catch (err) {
setError(err.message || 'Failed to create token');
} finally {
setLoading(false);
}
};
Error Display Pattern
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4 mb-4">
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
Authentication Patterns
Login Service
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';
export const authService = {
async login(username, password) {
const response = await fetch(`${BASE_URL}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Login failed' }));
throw new Error(error.message || 'Invalid credentials');
}
return await response.json();
}
};
Token Storage
const { token, user } = await authService.login(username, password);
localStorage.setItem('jwt', token);
const token = localStorage.getItem('jwt');
localStorage.removeItem('jwt');
Response Format Handling
Flexible Response Parsing
Some API endpoints return different response formats. Handle both:
const response = await get(endpoint, token, tenantId, projectId);
const keys = response.keys || response.items || response;
const response = await post(endpoint, data, token, tenantId, projectId);
const token = response.token || response.data || response;
Testing API Integration
MSW Handler Setup
import { http, HttpResponse } from 'msw';
const BASE_URL = 'http://localhost:4000';
export const handlers = [
http.get(`${BASE_URL}/api/v1/tenants/:tenantId/projects/:projectId/tokens`, ({ request }) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return HttpResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const tenantId = request.headers.get('X-Tenant-ID');
if (!tenantId) {
return HttpResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
);
}
return HttpResponse.json({ tokens: mockTokens });
})
];
Common Pitfalls
❌ Missing Authentication Header
await fetch('/api/v1/tokens');
await get('/tenants/:id/projects/:id/tokens', token, tenantId, projectId);
❌ Hardcoded Base URL
await fetch('http://localhost:4000/api/v1/tokens');
const BASE_URL = import.meta.env.VITE_API_URL;
❌ Not Handling 204 No Content
const data = await response.json();
if (response.status === 204) {
return null;
}
return await response.json();
❌ Swallowing Errors
try {
await apiCall();
} catch (error) {
}
try {
await apiCall();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
Checklist for New Service
When creating a new service file:
Resource Files