Instruction file imported from giventadevelop/mosc-redesign (
.cursor/rules/nextjs_api_routes.mdc). Copyright stays with the author.
-
All API routes must...
- Use authentication middleware
- Return JSON responses
- etc.
-
No local DTO/interface declarations
- All DTOs should be imported from
@/typesif needed (none should be declared locally) - No DTO redeclaration: All DTOs should be imported from
@/types(none needed in this handler) - Example:
// ✅ DO: Import DTOs import type { UserProfileDTO } from '@/types'; // ❌ DON'T: Redeclare DTOs // interface UserProfileDTO { ... }
- All DTOs should be imported from
-
Consistent environment variable
- Use
process.env.NEXT_PUBLIC_API_BASE_URLfor the backend API base URL, matching the rest of your proxy routes - Do not use hardcoded URLs or other env vars for backend base URL
- Example:
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
- Use
-
Query string handling
- Use a
buildQueryStringhelper to forward all query params, just like in the user profile proxy - Ensures all filters, pagination, and sorts are preserved
- Example:
function buildQueryString(query: Record<string, any>) { const params = new URLSearchParams(); for (const key in query) { const value = query[key]; if (Array.isArray(value)) { value.forEach(v => params.append(key, v)); } else if (typeof value !== 'undefined') { params.append(key, value); } } return params.toString(); }
- Use a
-
Do NOT add tenantId.equals in your client/server code when calling the proxy
- The proxy handler will always inject tenantId.equals automatically.
- Only add tenantId.equals if you are calling the backend API directly (not via /api/proxy/...).
- This prevents duplicate tenantId.equals parameters and backend criteria errors.
- Example:
// ✅ DO: Only add email.equals or userId.equals const params = new URLSearchParams({ 'email.equals': email }); await fetch('/api/proxy/user-profiles?' + params.toString()); // ❌ DON'T: Add tenantId.equals when calling the proxy const params = new URLSearchParams({ 'email.equals': email, 'tenantId.equals': tenantId }); await fetch('/api/proxy/user-profiles?' + params.toString()); // Will result in duplicate tenantId
-
JWT handling
- Use
fetchWithJwtRetryfor all backend calls, ensuring robust authentication and retry logic - Do not call backend APIs directly with fetch; always use the helper
- Example:
import { getCachedApiJwt, generateApiJwt } from '@/lib/api/jwt'; async function fetchWithJwtRetry(apiUrl: string, options: any = {}, debugLabel = '') { let token = await getCachedApiJwt(); let response = await fetch(apiUrl, { ...options, headers: { ...options.headers, Authorization: `Bearer ${token}`, }, }); if (response.status === 401) { token = await generateApiJwt(); response = await fetch(apiUrl, { ...options, headers: { ...options.headers, Authorization: `Bearer ${token}`, }, }); } return response; }
- Use
-
Error handling
- Catch and log errors, returning a 500 with a clear message if something goes wrong
- Example:
try { // ... } catch (err) { console.error('Proxy error:', err); res.status(500).json({ error: 'Internal server error', details: String(err) }); }
-
Method handling
- Only allow GET and POST (or appropriate methods), with proper 405 responses for others
- Example:
if (req.method === 'GET') { /* ... */ } else if (req.method === 'POST') { /* ... */ } else { res.setHeader('Allow', ['GET', 'POST']); res.status(405).end(`Method ${req.method} Not Allowed`); }
-
Required backend fields for create operations
- When creating resources via proxy API routes, all fields required by the backend (including timestamps like
createdAtandupdatedAt) must be included in the payload, even if not set by the client. - The proxy or client must ensure these fields are present to avoid backend validation errors (e.g., Spring Boot will reject null
createdAt/updatedAt). - Example for ticket type creation:
// ✅ DO: Include all required fields const payload = { ...form, eventId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; await fetch('/api/proxy/ticket-types', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), });
- When creating resources via proxy API routes, all fields required by the backend (including timestamps like
-
ID field handling for create operations
- For POST (create) operations, do not include the 'id' field in the payload (or set it to null if required by the backend).
- Only include 'id' for update (PUT/PATCH) operations.
- This matches backend expectations and avoids sending unnecessary or misleading ids during creation.
- Example for ticket type creation:
// ✅ DO: Omit 'id' for create const { id, ...rest } = form; const payload = { ...rest, event: { id: eventId }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; await fetch('/api/proxy/ticket-types', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); - Clarification: If the backend requires the 'id' field to be present (but null) for POST requests, explicitly set
id: nullin the payload. This is sometimes required by strict backend validation or OpenAPI schemas. - Example for explicit null id:
// ✅ DO: Set id: null if backend requires it const { id, ...rest } = form; const payload = { ...rest, id: null, // Explicitly set to null for backend compatibility event: { id: eventId }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; await fetch('/api/proxy/ticket-types', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), });
-
Pattern Used
- This matches the pattern in:
src/pages/api/proxy/user-profiles/index.tssrc/components/ProfileForm.tsx- This rule file itself
- This matches the pattern in:
-
Correct Implementation Example
// src/pages/api/proxy/ticket-types/index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { getCachedApiJwt, generateApiJwt } from '@/lib/api/jwt'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; function buildQueryString(query: Record<string, any>) { /* ... */ } async function fetchWithJwtRetry(apiUrl: string, options: any = {}, debugLabel = '') { /* ... */ } export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { if (req.method === 'GET') { const qs = buildQueryString(req.query); const apiUrl = `${API_BASE_URL}/api/ticket-types${qs ? `?${qs}` : ''}`; const response = await fetchWithJwtRetry(apiUrl, { method: 'GET' }); const data = await response.json(); res.status(response.status).json(data); } else if (req.method === 'POST') { const apiUrl = `${API_BASE_URL}/api/ticket-types`; const response = await fetchWithJwtRetry(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req.body), }); const data = await response.json(); res.status(response.status).json(data); } else { res.setHeader('Allow', ['GET', 'POST']); res.status(405).end(`Method ${req.method} Not Allowed`); } } catch (err) { console.error('Proxy error:', err); res.status(500).json({ error: 'Internal server error', details: String(err) }); } } -
Anti-patterns
// ❌ DON'T: Redeclare DTOs or use hardcoded URLs interface TicketTypeDTO { ... } const API_BASE_URL = 'http://localhost:8080'; // ❌ DON'T: Call fetch directly without JWT helper const response = await fetch(apiUrl, { ... }); -
Rule Maintenance
- Update this rule when new patterns emerge
- Add examples from actual codebase
- Remove outdated patterns
- Cross-reference related rules
-
Automated tenantId injection in DTOs
- Always include the
tenantIdfield in every DTO sent to the backend. The value must come from the environment variableNEXT_PUBLIC_TENANT_ID. - Use a centralized helper to read the tenant ID (see
getTenantIdinsrc/lib/env.ts). - Use the
withTenantIdutility (src/lib/withTenantId.ts) to inject the tenantId into any DTO before sending it to the backend. This ensures consistency and prevents missing tenant IDs. - Never hardcode the tenantId or set it manually in multiple places.
- Example usage:
import { withTenantId } from '@/lib/withTenantId'; const payload = withTenantId({ ...formData, // other fields }); await fetch('/api/proxy/some-endpoint', { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, }); - The environment variable must be set in
.env.localas:NEXT_PUBLIC_TENANT_ID=your-tenant-id - The helper must throw a clear error if the variable is missing, to prevent silent failures.
- This pattern is required for all multi-tenant API calls and DTOs.
- See also:
src/lib/env.ts,src/lib/withTenantId.tsfor implementation details.
- Always include the
-
TenantId injection in all proxy API routes (cross-cutting enforcement)
- All proxy API routes (e.g., /api/proxy/event-details, /api/proxy/ticket-types, etc.) must use the shared
createProxyHandlerfromsrc/lib/proxyHandler.ts. - This handler automatically injects
tenantIdinto all DTOs for POST/PUT/PATCH requests using thewithTenantIdutility. - Example usage:
import { createProxyHandler } from '@/lib/proxyHandler'; export default createProxyHandler({ backendPath: '/api/event-details' }); - Rationale:
- Guarantees that every create/update request includes the correct tenantId, regardless of frontend implementation.
- Prevents accidental omission of tenantId in multi-tenant environments.
- Centralizes error handling, JWT logic, and query string forwarding.
- Backend enforcement:
- The backend (Rust API) should also validate that tenantId is present in every DTO and reject requests if missing.
- See also:
withTenantIdutility, Rust validation snippet below.
- All proxy API routes (e.g., /api/proxy/event-details, /api/proxy/ticket-types, etc.) must use the shared
-
Add
[...slug].tsproxies for single resource operations using shared handler- For every backend resource that supports single-resource operations (e.g., GET/PUT/DELETE by ID), add a
[...slug].tsfile in the corresponding proxy API directory. - The handler must use the shared
createProxyHandlerfrom@/lib/proxyHandler, passing the correctbackendPath(e.g.,/api/event-details). - Remove all custom logic and unused imports from these handlers; the shared handler centralizes JWT, tenantId, error, and query param logic.
- Examples:
// src/pages/api/proxy/event-details/[...slug].ts import { createProxyHandler } from '@/lib/proxyHandler'; export default createProxyHandler({ backendPath: '/api/event-details' }); // src/pages/api/proxy/event-medias/[...slug].ts import { createProxyHandler } from '@/lib/proxyHandler'; export default createProxyHandler({ backendPath: '/api/event-medias' }); - This ensures all single-resource proxy routes are DRY, secure, and multi-tenant aware by default.
- Rationale:
- Prevents code duplication and errors in per-route logic
- Guarantees tenantId injection and JWT handling for all resource operations
- Simplifies maintenance and onboarding for new resources
- For every backend resource that supports single-resource operations (e.g., GET/PUT/DELETE by ID), add a
-
JHipster/Spring Data REST filter syntax for criteria queries
- When calling backend APIs that use JHipster or Spring Data REST criteria objects, always use the correct filter syntax:
field.operation=value(e.g.,tenantId.equals=tenant_demo_001). - Do not use just
tenantId=...orfield=...for filter fields; this will cause type conversion errors in the backend. - Common operations:
.equals,.contains,.in, etc. (e.g.,userStatus.equals=ACTIVE,email.contains=gmail.com) - Example:
// ✅ DO: Use .equals for exact match params.append('tenantId.equals', getTenantId()); // ✅ DO: Use .contains for substring match params.append('email.contains', 'gmail.com'); // ❌ DON'T: Use just tenantId=... params.append('tenantId', getTenantId()); // Will cause backend error - This applies to all criteria-based GET endpoints, especially for multi-tenant filtering and user/resource queries.
- See also: UserProfileCriteria in backend code for supported filters.
- When calling backend APIs that use JHipster or Spring Data REST criteria objects, always use the correct filter syntax:
-
All authenticated fetches must be server-side
- Never fetch authenticated resources (e.g., user profiles, protected APIs) directly from the client. Always perform these fetches in a server component, server action, or API route.
- Rationale: Only server-side code has access to the user's session and can generate/attach a valid JWT. Client-side fetches will not have the session and will result in 401 Unauthorized errors.
- Example:
// ✅ DO: Fetch admin profile server-side export default async function ManageUsagePage() { const { userId } = auth(); const adminProfile = userId ? await fetchAdminProfileServer(userId) : null; // ... } // ❌ DON'T: Fetch admin profile in useEffect or client-side hooks useEffect(() => { fetch('/api/proxy/user-profiles/by-user/' + userId); }, [userId]); - See also: ProfileBootstrapper, ProfileForm, ManageUsagePage for correct patterns.
-
All authenticated API calls must be made from server actions or server components, not from client components
- Never call protected proxy API endpoints (e.g., /api/proxy/user-profiles, /api/proxy/ticket-types, etc.) directly from client components.
- Always create a server action (e.g., actions.ts) or use a server component to perform the API call, then pass the result to the client component as props or via server action invocation.
- This ensures JWT/session is available, tenantId is injected, and security is enforced.
- Example:
// src/app/admin/manage-usage/actions.ts export async function patchUserProfileServer(userId: number, payload: any) { /* ... */ } // src/app/admin/manage-usage/ManageUsageClient.tsx import { patchUserProfileServer } from './actions'; // ... await patchUserProfileServer(user.id, payload); - See also: ProfileBootstrapper, ProfileForm, ManageUsagePage for correct patterns.
-
Client Components Must Not Make Direct API Calls
- Client components (marked with 'use client') must NEVER make direct fetch calls to API endpoints.
- This includes both proxy endpoints (/api/proxy/...) and direct backend calls.
- Client components should only:
- Receive data as props from server components
- Call server actions for mutations
- Handle UI state and user interactions
- Rationale: Client components run in the browser where:
- Environment variables may not be available
- JWT tokens and session data are not accessible
- Direct API calls will fail with authentication errors
- Correct Pattern:
// ✅ DO: Server component fetches data // src/app/profile/page.tsx (server component) export default async function ProfilePage() { const profile = await fetchProfileServer(userId); return <ProfileForm profile={profile} />; } // ✅ DO: Client component receives props // src/components/ProfileForm.tsx (client component) 'use client'; export function ProfileForm({ profile }: { profile: UserProfileDTO }) { // Handle form state and UI interactions only } // ✅ DO: Client component calls server action // src/components/ProfileForm.tsx import { updateProfileServer } from './actions'; const handleSubmit = async (data: any) => { await updateProfileServer(data); }; - Anti-patterns:
// ❌ DON'T: Client component making direct API calls 'use client'; export function ProfileForm() { useEffect(() => { fetch('/api/proxy/user-profiles/by-user/' + userId); }, [userId]); } - References:
- See
src/components/ProfileForm.tsxfor problematic implementation - See
src/components/DashboardContent.tsxfor problematic implementation - See
src/app/admin/manage-usage/ManageUsageClient.tsxfor correct pattern
- See
-
Standard: Place all server-side API calls in ApiServerActions.ts
- If your module makes authenticated or protected API calls, create a file named
ApiServerActions.tsin that folder. - Place all server-side API calls (fetch, patch, post, etc.) in this file as exported async functions.
- Import and use these actions from your client components.
- This ensures all API calls are server-side, JWT/session is available, and security is enforced.
- Example:
// src/app/admin/manage-usage/ApiServerActions.ts export async function patchUserProfileServer(userId: number, payload: any) { /* ... */ } // src/app/admin/manage-usage/ManageUsageClient.tsx import { patchUserProfileServer } from './ApiServerActions'; // ... await patchUserProfileServer(user.id, payload); - This is now the standard for all modules with API calls.
- If your module makes authenticated or protected API calls, create a file named
PATCH/PUT Server Actions: Direct Backend Update Pattern (Service JWT, No Proxy)
- For PATCH/PUT operations that update backend resources, prefer direct backend calls from server actions using a service JWT, not via the proxy, when sessionless service access is required.
- Use
getCachedApiJwt()(andgenerateApiJwt()as fallback) to obtain a service JWT. - Always include the
idfield in the payload for PATCH/PUT, as required by backend conventions. - Set
Content-Type: application/merge-patch+jsonfor PATCH (orapplication/jsonfor PUT if required by backend). - Attach the JWT as an
Authorizationheader:Bearer <token>. - Do not rely on Clerk session or cookies for these calls.
- Example:
import { getCachedApiJwt, generateApiJwt } from '@/lib/api/jwt'; export async function patchResourceServer(resourceId: number, payload: Partial<ResourceDTO>) { const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; const url = `${API_BASE_URL}/api/resource/${resourceId}`; let token = await getCachedApiJwt(); if (!token) token = await generateApiJwt(); const finalPayload = { ...payload, id: resourceId }; const res = await fetch(url, { method: 'PATCH', headers: { 'Content-Type': 'application/merge-patch+json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(finalPayload), }); if (!res.ok) throw new Error(await res.text()); return res.json(); } - See
patchUserProfileServerandupdateEventTicketTransactionCheckInfor real examples. - This pattern is required for all PATCH/PUT server actions that do not require user session context.
- Use
Webhooks REST API Call Pattern (Stripe, etc.)
- When making REST API calls from webhook files (such as Stripe webhooks in src/app/api/webhooks/stripe/route.ts), do NOT use the standard /api/proxy/... pattern or proxy API handler.
- Instead, call the backend Rust API directly from the webhook file using fetch, and:
- Always include the JWT token in the Authorization header: 'Authorization: Bearer '
- Always pass the 'id' field and all required fields in the PATCH/PUT/POST payload, matching the backend DTO requirements.
- Use 'Content-Type: application/merge-patch+json' for PATCH requests (or 'application/json' for POST/PUT as required).
- Do not rely on Clerk session or cookies; use service JWT only.
- See handleChargeFeeUpdate in src/app/api/webhooks/stripe/route.ts for a reference implementation.
- Rationale: Webhook files run in a different context and must not use the proxy API pattern. This ensures correct authentication and data integrity for backend updates.
- Project-wide convention: REST API calls from server action scripts use nextjs_api_routes.mdc. Webhook files must follow this direct-call pattern for backend updates.
-
Next.js 15+ Dynamic Route Async Context Rule
- In app router page, layout, or route handlers, always
awaitany async context objects such asparams,headers(), orcookies()before using their properties. - This is required in Next.js 15+ where these objects may be promises.
- DO:
export default async function Page(props: { params: { id: string } }) { const { params } = props; // If params is a promise, await it const resolvedParams = typeof params.then === 'function' ? await params : params; const id = resolvedParams.id; // ... } - DON'T:
// ❌ DON'T: Use params.id directly if params may be a promise const id = params.id; // May throw in Next.js 15+ - Rationale:
- Prevents runtime errors like "params should be awaited before using its properties".
- Ensures compatibility with Next.js 15+ dynamic route context.
- See: https://nextjs.org/docs/messages/sync-dynamic-apis
- In app router page, layout, or route handlers, always
-
TenantId Query Parameter and Body Injection Refinement
- Only add
tenantId.equalsas a query parameter for list/filter endpoints if the REST API schema requires it (as specified in design/requirements). Do not inject to every list/filter request by default. - Only inject
tenantIdinto the request body if the DTO defines it (i.e., if the field exists in the request body object). Do not addtenantIdto the body if the DTO does not have atenantIdfield. - Rationale: Prevents backend errors and ensures compliance with API contracts.
- Example:
// ✅ DO: Add tenantId.equals only if required by the API schema const qs = new URLSearchParams(); if (shouldAddTenantIdEquals) qs.append('tenantId.equals', tenantId); // ✅ DO: Inject tenantId into body only if field exists if ('tenantId' in dto) dto.tenantId = tenantId; - See also: proxy handler implementation for conditional logic.
- Only add
PATCH/PUT/DELETE Proxy Handler Body Parser Rule
-
For all API proxy handlers in
[...slug].ts(or any dynamic route handler that supports PATCH/PUT/DELETE), you MUST set:export const config = { api: { bodyParser: false, }, }; -
This disables the default body parser, allowing raw JSON/merge-patch+json payloads to be forwarded to the backend.
-
Without this, PATCH/PUT requests may hang or fail, especially for large or non-standard payloads.
-
This matches the pattern in
src/pages/api/proxy/event-ticket-transactions/[...slug].tsand is required for all similar endpoints. -
References:
- See
src/pages/api/proxy/event-ticket-transactions/[...slug].tsfor a working example. - See
src/pages/api/proxy/discount-codes/[...slug].tsfor the required fix.
- See
-
Port-Agnostic App URL Configuration
- Use
getAppUrl()from@/lib/envinstead of hardcoded URLs for server-side API calls. - This ensures the application works on any port (3000, 3001, etc.) without hardcoding.
- DO:
import { getAppUrl } from '@/lib/env'; const baseUrl = getAppUrl(); const response = await fetch(`${baseUrl}/api/proxy/event-details`); - DON'T:
// ❌ DON'T: Hardcode port numbers const baseUrl = 'http://localhost:3000'; // ❌ DON'T: Use environment variable with hardcoded fallback const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; - Rationale:
- Makes the application truly port-agnostic
- Supports development on any port (3000, 3001, 3002, etc.)
- Maintains compatibility with production environment variables
- References:
- See
src/app/page.tsxfor correct usage - See
src/lib/env.tsfor thegetAppUrl()implementation
- See
- Use
-
Graceful API Failure Handling
- Always handle API fetch failures gracefully in server components to prevent page crashes.
- Use try-catch blocks around all API calls and provide fallback data.
- Log errors for debugging but don't let them break the user experience.
- DO:
async function fetchData() { try { const response = await fetch('/api/proxy/some-endpoint'); if (response.ok) { return await response.json(); } } catch (error) { console.error('API fetch failed:', error); } return []; // Return empty array or default data } - DON'T:
// ❌ DON'T: Let API failures crash the page const data = await fetch('/api/proxy/some-endpoint'); return data.json(); // This will throw if fetch fails - Common Causes of Fetch Failures:
- Backend API not running
- Missing environment variables (API credentials, tenant ID)
- Network connectivity issues
- Authentication failures
- Debugging Steps:
- Check if backend API is running
- Verify environment variables are set
- Check network connectivity
- Review proxy route logs
- References:
- See
src/app/page.tsxfor graceful error handling example
- See
-
Public Page API Call Pattern: Avoid Server Actions from Client Components
- For public pages (like homepage) that don't require authentication, avoid calling server actions from client components to prevent Next.js 15+
headers()async context errors. - Instead, use direct
fetch()calls to proxy endpoints that are listed in the public routes. - Rationale: Server actions called from client components can trigger
headers()without awaiting, causing runtime errors in Next.js 15+. - DO:
// ✅ DO: Use direct fetch to proxy endpoints from client components on public pages 'use client'; export function TeamSection() { useEffect(() => { const loadData = async () => { const baseUrl = getAppUrl(); const response = await fetch( `${baseUrl}/api/proxy/executive-committee-team-members?isActive.equals=true&sort=priorityOrder,asc`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, cache: 'no-store', } ); if (response.ok) { const data = await response.json(); setData(Array.isArray(data) ? data : []); } }; loadData(); }, []); } - DON'T:
// ❌ DON'T: Call server actions from client components on public pages 'use client'; export function TeamSection() { useEffect(() => { const loadData = async () => { const data = await fetchExecutiveTeamMembersServer(); // Server action - causes headers() error setData(data); }; loadData(); }, []); } - When to Use This Pattern:
- Public pages (homepage, event pages, etc.) that don't require authentication
- Client components that need to fetch data on mount
- When the proxy endpoint is in the public routes list
- When NOT to Use:
- Protected/admin pages that require authentication
- Server components (use server actions normally)
- When you need JWT authentication (use server actions with proper auth)
- References:
- See
src/components/TeamSection.tsxfor correct implementation - See
src/components/charity-sections/TeamSection.tsxfor correct implementation - See
src/middleware.tsfor public routes configuration
- See
- For public pages (like homepage) that don't require authentication, avoid calling server actions from client components to prevent Next.js 15+
-
STRICT RULE: All Server Actions Must Use fetchWithJwtRetry for Backend API Calls
- CRITICAL: All server actions that make backend API calls MUST use
fetchWithJwtRetryfrom@/lib/proxyHandler. - NEVER use direct
fetch()calls to backend APIs in server actions. - NEVER implement custom JWT retry logic in server actions.
- Rationale: This ensures consistent authentication, error handling, and prevents
headers()async context errors in Next.js 15+. - DO:
// ✅ DO: Use fetchWithJwtRetry for all backend API calls import { fetchWithJwtRetry } from '@/lib/proxyHandler'; export async function fetchDataServer() { const res = await fetchWithJwtRetry(`${API_BASE_URL}/api/some-endpoint`, { cache: 'no-store', }); if (!res.ok) return null; return await res.json(); } - DON'T:
// ❌ DON'T: Use direct fetch with custom JWT logic let token = await getCachedApiJwt(); let res = await fetch(`${API_BASE_URL}/api/some-endpoint`, { headers: { 'Authorization': `Bearer ${token}` }, }); if (res.status === 401) { token = await generateApiJwt(); res = await fetch(`${API_BASE_URL}/api/some-endpoint`, { headers: { 'Authorization': `Bearer ${token}` }, }); } // ❌ DON'T: Use direct fetch without JWT const res = await fetch(`${API_BASE_URL}/api/some-endpoint`); - Exception: Only use direct
fetch()for proxy endpoints (e.g.,/api/proxy/...) that are in the public routes list. - Enforcement: This rule applies to ALL server actions in the codebase. Any server action found using direct
fetch()to backend APIs will be flagged for immediate correction. - References:
- See
src/lib/proxyHandler.tsfor thefetchWithJwtRetryimplementation - See
src/app/admin/events/[id]/media/ApiServerActions.tsfor correct usage - See
src/app/admin/ApiServerActions.tsfor correct usage
- See
- CRITICAL: All server actions that make backend API calls MUST use