rule-frontend-services
MANDATORY when editing files matching ["frontend/src/services/**/*.ts"]. When working on API service integration in frontend/src/services/
Files1
View on GitHubSKILL.md.claude/skills/rule-frontend-services/SKILL.md
GitHubFrontend Services Rules
Centralized Fetch Factory
ALWAYS create API client functions using requesterForEndpoint() with an EndpointConfig object. NEVER write raw fetch() calls to the backend API from server-side code.
Example from codebase:
// From frontend/src/services/fetch/endpointConfigs.ts
export const fetchApplicationsEndpoint = {
basePath: environment.API_URL,
version: "alpha",
namespace: "applications",
method: "POST" as ApiMethod,
};
// From frontend/src/services/fetch/fetchers/fetchers.ts
export const fetchCompetition = cache(
requesterForEndpoint(fetchCompetitionEndpoint),
);
Thin Domain Fetcher Wrappers
ALWAYS create a dedicated fetcher file in src/services/fetch/fetchers/ for each API resource. MUST handle session retrieval, header construction, and response extraction.
Example from codebase:
// From frontend/src/services/fetch/fetchers/applicationsFetcher.ts
"server-only";
export const getApplications = async (
token: string,
userId: string,
): Promise<ApplicationDetail[]> => {
const ssgToken = { "X-SGG-Token": token };
const resp = await fetchUserWithMethod("POST")({
subPath: `${userId}/applications`,
additionalHeaders: ssgToken,
body: { pagination: { page_offset: 1, page_size: 5000 } },
});
const json = (await resp.json()) as { data: [] };
return json.data;
};
"server-only" Directive on Server Fetchers
ALWAYS add the "server-only" directive at the top of server-side fetcher files. NEVER allow server-only code to be imported in client components.
Example from codebase:
// From frontend/src/services/fetch/fetchers/applicationsFetcher.ts
"server-only";
import { UnauthorizedError } from "src/errors";
import { getSession } from "src/services/auth/session";
useClientFetch for Client-Side Requests
ALWAYS use useClientFetch<T>() for client-side API requests. NEVER use raw fetch() in client components.
Example from codebase:
// From frontend/src/components/workspace/OrganizationInvitationReply.tsx
const { clientFetch } = useClientFetch<OrganizationInvitation>(
"unable to respond to invitation",
);
clientFetch(
`/api/user/organization-invitations/${id}`,
{ method: "POST", body: JSON.stringify({ accepted }) },
).then((response) => setInvitationStatus(response.status));
Promise.all for Parallel Fetches in RSC Pages
ALWAYS fetch data in parallel using Promise.all. ALWAYS name unresolved promises as varNamePromise.
Example from codebase:
// From frontend/src/app/[locale]/workspace/page.tsx
const session = await getSession();
const userRolesPromise = getUserPrivileges(session.token, session.user_id);
const userOrganizationsPromise = getUserOrganizations(session.token, session.user_id);
try {
[userRoles, userOrganizations] = await Promise.all([
userRolesPromise,
userOrganizationsPromise,
]);
} catch (e) {
console.error("Unable to fetch user details", e);
}
X-SGG-Token Header for Authenticated Requests
ALWAYS pass the user's token via an X-SGG-Token header for authenticated server-side API calls. NEVER pass tokens in the request body or query params.
Example from codebase:
// From frontend/src/services/fetch/fetchers/userFetcher.ts
const ssgToken = { "X-SGG-Token": token };
await fetchUserWithMethod("POST")({
subPath: `${userId}/can_access`,
additionalHeaders: ssgToken,
body: { resource_type: resourceType, privilege: "manage_org_members" },
});
getSession() as Sole Server-Side Auth Entry Point
ALWAYS use getSession() from src/services/auth/session.ts for server-side auth. ALWAYS check for null session and missing token before authenticated operations.
Example from codebase:
// From frontend/src/services/fetch/fetchers/applicationsFetcher.ts
export const fetchApplications = async (): Promise<ApplicationDetail[]> => {
const session = await getSession();
if (!session || !session.token) {
throw new UnauthorizedError("No active session");
}
return await getApplications(session.token, session.user_id);
};
ApiRequestError and parseErrorStatus
ALWAYS use ApiRequestError from src/errors.ts for API error handling. ALWAYS use parseErrorStatus() to extract HTTP status codes. MUST return <NotFound /> for 404 errors.
Example from codebase:
// From frontend/src/app/[locale]/opportunity/[id]/page.tsx
try {
const response = await getCompetitionDetails(id);
} catch (error) {
if (parseErrorStatus(error as ApiRequestError) === 404) {
return <NotFound />;
}
return <TopLevelError />;
}
{ data: T } Response Envelope
ALWAYS type API responses using the { data: T } envelope pattern. ALWAYS extract .data before returning.
Example from codebase:
// From frontend/src/services/fetch/fetchers/organizationFetcher.ts
const json = (await resp.json()) as { data: UserRole[] };
return json.data;
Types: Prefer type over interface
ALWAYS place shared types in src/types/. ALWAYS use type by default. ONLY use interface when the type will be extended.
Example from codebase:
// From frontend/src/types/userTypes.ts
export type OrganizationInvitation = {
organization_invitation_id: string;
status: string;
created_at: string;
};
export interface RoleDefinition { role_id: string; role_name: string; }
export interface UserRole extends RoleDefinition { privileges: Privileges[]; }
Zod Validation for Server Actions
ALWAYS use Zod schemas for server-side form validation. ALWAYS use safeParse() and return error.flatten().fieldErrors. ALWAYS use translated validation messages.
Example from codebase:
// From frontend/src/app/actions/inviteUserAction.ts
const schema = z.object({
email: z.string().min(1, { message: t("email") }),
role: z.string().min(1, { message: t("role") }),
});
const validatedFields = schema.safeParse({
email: formData.get("email"),
role: formData.get("role"),
});
if (!validatedFields.success) {
return validatedFields.error.flatten().fieldErrors;
}
withFeatureFlag HOC for Feature-Gated Pages
ALWAYS use withFeatureFlag(Component, "flagName", fallback) to gate pages. ALWAYS name flags in camelCase with a boolean-off default.
Example from codebase:
// From frontend/src/app/[locale]/organization/[id]/page.tsx
export default withFeatureFlag<OrganizationDetailPageProps, never>(
OrganizationDetailPage,
"userAdminOff",
() => redirect("/maintenance"),
);
AuthorizationGate and AuthenticationGate
ALWAYS use AuthenticationGate for login-required pages. ALWAYS use AuthorizationGate for privilege-required pages. NEVER perform client-side privilege checking.
Example from codebase:
// From frontend/src/app/[locale]/organization/[id]/page.tsx
<AuthorizationGate
resourcePromises={{
organizationDetails: getOrganizationDetails(session?.token || "", id),
}}
requiredPrivileges={[{
resourceId: id,
resourceType: "organization",
privilege: "manage_org_members",
}]}
onUnauthorized={() => <UnauthorizedMessage />}
>
<OrganizationDetail organizationId={id} />
</AuthorizationGate>
Context Enrichment
When generating significant service code (new fetcher, new API integration, auth changes), enrich your context:
- Call
get_architecture_section("frontend")from thesimpler-grants-contextMCP server to understand frontend service layer principles - Call
get_rule_detail("api-error-handling")for how the API surfaces errors that the frontend must handle - Consult Compound Knowledge for indexed documentation on fetch patterns, error handling, and auth integration
Related Rules
When working on frontend services, also consult these related rules:
frontend-hooks.mdc—useClientFetchhook that components use for client-side requestsfrontend-components.mdc— how components consume server-side fetchers and handle errorsapi-error-handling.mdc— API error response format that frontend must parse (ValidationErrorDetail)cross-domain.mdc— feature flags, structured error patterns
Specialist Validation
When generating or significantly modifying frontend service code:
For simple changes (< 20 lines, adding a header): No specialist invocation needed — the directives in this rule file are sufficient.
For moderate changes (new fetcher function, new endpoint config):
Invoke codebase-conventions-reviewer to validate against service conventions.
For complex changes (new auth flow, error handling overhaul, new service pattern): Invoke the following specialists (run in parallel where possible):
security-sentinel— validate token handling, no credentials in URLs/logs, proper auth flowperformance-oracle— validate fetch patterns, caching, parallel request efficiencykieran-typescript-reviewer— TypeScript-specific quality review