BettaResume uses tRPC v11 with React Query integration. The API runs as a Cloudflare Worker on port 4000.
- Development:
http://localhost:4000/trpc - Production: Configured via
NEXT_PUBLIC_API_URL
All protected endpoints require a valid Clerk JWT in the Authorization header:
Authorization: Bearer <clerk_session_token>
The frontend automatically includes this header via the tRPC client configuration.
Syncs the currently authenticated Clerk user to the database.
Type: Mutation (protected)
Input: None
Output:
{
id: string; // Clerk user ID
email: string; // User's email
createdAt: Date;
updatedAt: Date;
}Frontend Usage:
const syncMutation = trpc.auth.syncUser.useMutation();
await syncMutation.mutateAsync();Get all resumes for the authenticated user.
Type: Query (protected)
Input: None
Output:
Array<{
id: string;
title: string;
metadata: ResumeMetadata;
createdAt: Date;
updatedAt: Date;
}>Frontend Usage:
const { data: resumes, isLoading } = trpc.resume.list.useQuery();Get a single resume with all its sections.
Type: Query (protected)
Input:
{ id: string }Output:
{
id: string;
title: string;
metadata: ResumeMetadata;
sections: Array<{
id: string;
type: SectionType;
content: object;
displayOrder: number;
isVisible: boolean;
}>;
createdAt: Date;
updatedAt: Date;
}Frontend Usage:
const { data: resume } = trpc.resume.getById.useQuery({ id: resumeId });Create a new resume.
Type: Mutation (protected)
Input:
{
title: string;
metadata?: ResumeMetadata; // Optional, uses defaults
}Output:
{
id: string;
title: string;
metadata: ResumeMetadata;
createdAt: Date;
updatedAt: Date;
}Frontend Usage:
const createMutation = trpc.resume.create.useMutation({
onSuccess: () => {
utils.resume.list.invalidate();
},
});
await createMutation.mutateAsync({ title: 'My Resume' });Update an existing resume's metadata or title.
Type: Mutation (protected)
Input:
{
id: string;
title?: string;
metadata?: Partial<ResumeMetadata>;
}Output:
{
id: string;
title: string;
metadata: ResumeMetadata;
updatedAt: Date;
}Frontend Usage:
const updateMutation = trpc.resume.update.useMutation({
onSuccess: () => {
utils.resume.getById.invalidate({ id: resumeId });
},
});
await updateMutation.mutateAsync({
id: resumeId,
metadata: { personalInfo: { fullName: 'John Doe' } },
});Delete a resume and all its sections.
Type: Mutation (protected)
Input:
{ id: string }Output:
{ success: boolean }Frontend Usage:
const deleteMutation = trpc.resume.delete.useMutation({
onSuccess: () => {
utils.resume.list.invalidate();
},
});
await deleteMutation.mutateAsync({ id: resumeId });Create or update a section. Uses the section ID to determine insert vs update.
Type: Mutation (protected)
Input:
{
id?: string; // Omit for new section
resumeId: string;
type: SectionType;
content: object; // Type-specific content
displayOrder?: number;
isVisible?: boolean;
}Output:
{
id: string;
resumeId: string;
type: SectionType;
content: object;
displayOrder: number;
isVisible: boolean;
updatedAt: Date;
}Frontend Usage:
const upsertMutation = trpc.section.upsert.useMutation({
onSuccess: () => {
utils.resume.getById.invalidate({ id: resumeId });
},
});
await upsertMutation.mutateAsync({
resumeId,
type: 'experience',
content: { items: [...] },
});Delete a section.
Type: Mutation (protected)
Input:
{ id: string }Output:
{ success: boolean }Reorder sections within a resume.
Type: Mutation (protected)
Input:
{
resumeId: string;
sectionIds: string[]; // Ordered list of section IDs
}Output:
{ success: boolean }{
items: Array<{
id: string;
company: string;
position: string;
location?: string;
startDate: string;
endDate?: string;
current: boolean;
description: string; // HTML rich text
highlights?: string[];
}>;
}{
items: Array<{
id: string;
institution: string;
degree: string;
field?: string;
location?: string;
startDate: string;
endDate?: string;
gpa?: string;
description?: string;
honors?: string[];
}>;
}{
categories: Array<{
id: string;
name: string;
skills: string[];
}>;
}{
items: Array<{
id: string;
name: string;
description: string;
technologies?: string[];
url?: string;
startDate?: string;
endDate?: string;
}>;
}{
items: Array<{
id: string;
name: string;
issuer: string;
issueDate?: string;
expiryDate?: string;
credentialId?: string;
url?: string;
}>;
}{
items: Array<{
id: string;
title: string;
issuer: string;
date?: string;
description?: string;
}>;
}{
items: Array<{
id: string;
language: string;
proficiency: 'native' | 'fluent' | 'advanced' | 'intermediate' | 'basic';
}>;
}{
items: Array<{
id: string;
organization: string;
role: string;
startDate?: string;
endDate?: string;
description?: string;
}>;
}{
items: Array<{
id: string;
title: string;
publisher: string;
date?: string;
url?: string;
description?: string;
}>;
}{
items: Array<{
id: string;
name: string;
title: string;
company: string;
email?: string;
phone?: string;
relationship?: string;
}>;
}tRPC errors are typed and include:
{
code: 'UNAUTHORIZED' | 'NOT_FOUND' | 'BAD_REQUEST' | 'INTERNAL_SERVER_ERROR';
message: string;
}Frontend Error Handling:
const mutation = trpc.resume.create.useMutation({
onError: (error) => {
if (error.data?.code === 'UNAUTHORIZED') {
// Redirect to login
} else {
toast.error(error.message);
}
},
});Access React Query utils via trpc.useUtils():
const utils = trpc.useUtils();
// Invalidate queries
utils.resume.list.invalidate();
utils.resume.getById.invalidate({ id: resumeId });
// Prefetch
utils.resume.getById.prefetch({ id: resumeId });
// Set query data directly
utils.resume.getById.setData({ id: resumeId }, (old) => ({
...old,
title: 'New Title',
}));