원클릭으로
trpc
Conventions for writing tRPC routers and procedures.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Conventions for writing tRPC routers and procedures.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when changing or reviewing PrairieLearn element question.html contracts, including schema-level attributes/children, Python semantic validation, docs, and AI HTML validation support lists.
Write end-to-end browser tests using Playwright.
Backwards compatibility rules when changing element controllers in `apps/prairielearn/elements/`.
Dealing with groups and teams in PrairieLearn, and confusion about table/column/variable names.
| name | trpc |
| description | Conventions for writing tRPC routers and procedures. |
trpc.ts.trpc/assessment/), adjust the ResLocalsForPage type, context fields, URL helper in lib/client/url.ts, and mount path in server.ts.pages/instructorInstanceAdminSettings/). Key pieces: generatePrefixCsrfToken from @prairielearn/signed-token server-side, scope's client.ts + context.ts + QueryClientProviderDebug client-side.tRPC routers are mounted per authorization scope, not per page. Each scope has its own directory under apps/prairielearn/src/trpc/ and is mounted behind the corresponding Express authorization middleware chain.
| Scope | Directory | Mount path | ResLocalsForPage type |
|---|---|---|---|
| administrator | trpc/administrator/ | /pl/administrator/trpc | 'plain' |
| course | (create when needed) | /pl/course/:course_id/trpc | 'course' |
| course instance | trpc/courseInstance/ | /pl/course_instance/:id/instructor/trpc | 'course-instance' |
| assessment | trpc/assessment/ | .../assessment/:assessment_id/trpc | 'assessment' |
| assessment question | trpc/assessmentQuestion/ | .../assessment/:aid/assessment_question/:aqid/trpc | 'instructor-assessment-question' |
| assessment instance | (create when needed) | .../assessment_instance/:ai_id/trpc | 'assessment-instance' |
| instance question | (create when needed) | .../instance_question/:iq_id/trpc | 'instance-question' |
Do not create per-page tRPC routers.
Every scope directory contains: init.ts, trpc.ts, client.ts, context.ts, plus one *.ts file per subrouter and optional *.sql files for scope-specific queries. All routers use superjson as the transformer in both init.ts and client.ts.
See any existing scope (e.g. trpc/assessment/) for the exact boilerplate. The files follow a mechanical pattern — init.ts creates the tRPC instance and authorization middleware, trpc.ts composes subrouters and exports Express middleware via createExpressMiddleware, client.ts creates the HTTP client with CSRF headers, context.ts exports TRPCProvider/useTRPC via createTRPCContext.
list, upsert, destroy — not listWidgets or deleteMutation.Mutation/Procedure suffix (e.g. const setModeMutation = t.procedure...), but the router key must not: setMode: setModeMutation.{feature}Router (e.g. widgetsRouter). Router keys: camelCase (widgets).requireCourseInstancePermissionView, etc.) is defined in init.ts and chained with .use().features.enabled(...)) goes in the subrouter file, not init.ts. See trpc/assessment/access-control.ts for an example.createContext includes the full locals object alongside extracted fields as an escape hatch.lib/client/safe-db-types.ts (e.g. StaffStudentLabelSchema). Always .parse() records through the schema before returning.models/ instead of one-off SQL.trpc.subrouter.procedure.queryOptions() with useQuery() for queries. Do not manually construct { queryKey, queryFn } objects — the generated queryOptions() provides type-safe keys and avoids stale cache issues.trpc.subrouter.procedure.mutationOptions() with useMutation() for mutations.trpc proxy via the scope's useTRPC() hook from context.ts.The CSRF token is generated server-side with generatePrefixCsrfToken using the scope's URL helper from lib/client/url.ts, passed as a prop to the hydrated component, and sent by the tRPC client as an X-CSRF-Token header. The Express CSRF middleware validates it before the request reaches the tRPC router.
TRPCError. Use throwAppError only when the client needs extra fields beyond message (e.g. jobSequenceId to link to logs) or must branch on the code. A code that the client just renders as message is indistinguishable from UNKNOWN — don't add it.never for procedures that have no typed errors:
export interface AssessmentGroupsError {
AddGroup: never;
EnableGroupWork: { code: 'SYNC_JOB_FAILED'; jobSequenceId: string };
}
Don't use Record<string, never> or empty interface XError {}.message: string is added automatically by throwAppError and getAppError — only declare procedure-specific fields.getAppError<XError['Procedure']>(mutation.error) — procedure-keyed even when the entry is never. Never read raw mutation/query errors.<AppErrorAlert> or renderAppError, not {appError.message} directly. The exhaustive renderer map makes it a compile error to forget a variant or silently drop fields like jobSequenceId:
<AppErrorAlert
error={copyError}
onDismiss={() => copyMutation.reset()}
render={{
SYNC_JOB_FAILED: ({ message, jobSequenceId }) => (
<>
{message} <a href={`${urlPrefix}/jobSequence/${jobSequenceId}`}>View job logs</a>
</>
),
UNKNOWN: ({ message }) => message,
}}
/>
Use renderAppError(error, {...}) when you need a ReactNode to pass elsewhere (e.g. StickySaveBar's alert.message).pages/instructorStudentsLabels/components/LabelModifyModal.tsx (typed) and pages/administratorCourses/administratorCourses.html.tsx (never-keyed) for examples.tRPC procedures are tested as integration tests via HTTP. See tests/instructorStudentsLabels.test.ts for the pattern: create a client with generatePrefixCsrfToken + the scope's create*TrpcClient, call procedures directly, assert with TRPCClientError for error cases.