Implements a public REST API for SplitPro using @trpc/openapi, with OpenAPI 3.1 spec generation, interactive documentation (Scalar), and offset-based pagination - #707
Conversation
Adds an isolated public REST surface generated by @trpc/openapi (alpha): - apiRouter (curated subset) served at /api/v1 with API-key auth via createApiContext - ApiKey model (sha256-hashed, spro_ prefix) + create/list/revoke procedures + Account UI - OpenAPI spec at /api/openapi.json and Scalar docs at /api/docs - first exposed endpoint: user.me Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add shared pagination module (paginationInput schema + paginatedResult helper)
- Create API-specific paginated variants for getAllExpenses, getExpensesWithFriend,
getGroupExpenses, and getOwnExpenses (app procedures unchanged)
- All four endpoints return { items, pagination: { total, limit, offset, hasMore } }
- Defaults: limit=20, offset=0, max limit=100
- Post-process generated OpenAPI spec to inject default values for limit/offset
|
Would love to get an API for SplitPro 👍 |
krokosik
left a comment
There was a problem hiding this comment.
Tremendous work! Thank you and apologies for taking so long with the review. I've left some comments
| // Static spec generation needs only router types; env validation is disabled here so build/CI can run without app secrets. | ||
| process.env.SKIP_ENV_VALIDATION ||= 'true'; |
There was a problem hiding this comment.
This should be set in the script invocation not in the source code. You can use something like crossenv in package.json
There was a problem hiding this comment.
Can you setup a git push hook that rebuilds this file, making sure we have it always in sync?
| > | ||
| <div className="mt-4 flex flex-col gap-6"> | ||
| <p className="text-sm text-gray-400"> | ||
| Use API keys to access the SplitPro API. Send it as{' '} |
There was a problem hiding this comment.
While it is only required to add the english version of the text, it needs to be still used via i18n to support internationalization
| const isPlainValue = (v: string): string | number | boolean => { | ||
| if ('true' === v) { | ||
| return true; | ||
| } | ||
| if ('false' === v) { | ||
| return false; | ||
| } | ||
| const n = Number(v); | ||
| if (!Number.isNaN(n) && String(n) === v) { | ||
| return n; | ||
| } | ||
|
|
||
| return v; | ||
| }; |
There was a problem hiding this comment.
This belongs in a util file, we already have an api.ts one
|
|
||
| for (const op of Object.values(paths)) { | ||
| for (const operation of Object.values(op as Record<string, unknown>)) { | ||
| // Biome-ignore lint/suspicious/noExplicitAny: post-processing untyped OpenAPI JSON |
| <ApiKeys> | ||
| <AccountButton> | ||
| <KeyRound className="size-5 text-amber-500" /> | ||
| API keys |
| serializeDefaultSplit, | ||
| } from '~/lib/defaultSplit'; | ||
|
|
||
| export const getAllGroupsProcedure = protectedProcedure.query(async ({ ctx }) => { |
There was a problem hiding this comment.
Dang, is there no way to process the built router? I would like to minimize the change surface of this PR and future changes needed as much as possible
| const session: Session | null = user | ||
| ? { | ||
| user: toSessionUser(user), | ||
| expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(), |
There was a problem hiding this comment.
This should be configurable, preferably in the Api Key input
| const _apiRouter = createTRPCRouter({ | ||
| user: createTRPCRouter({ | ||
| me: meProcedure, | ||
| getFriends: getFriendsProcedure, | ||
| getOwnExpenses: getOwnExpensesApiProcedure, | ||
| getBalancesWithFriend: getBalancesWithFriendProcedure, | ||
| getFriend: getFriendProcedure, | ||
| }), | ||
| group: createTRPCRouter({ | ||
| getAllGroups: getAllGroupsProcedure, | ||
| getGroupDetails: getGroupDetailsProcedure, | ||
| }), | ||
| expense: createTRPCRouter({ | ||
| getAllExpenses: getAllExpensesApiProcedure, | ||
| getBalances: getBalancesProcedure, | ||
| getExpenseDetails: getExpenseDetailsProcedure, | ||
| getExpensesWithFriend: getExpensesWithFriendApiProcedure, | ||
| getGroupExpenses: getGroupExpensesApiProcedure, | ||
| addOrEditExpense: addOrEditExpenseApiProcedure, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Like I said before, please evaluate if processing the build router is feasible. If that is too much work, consider extracting the createTRPCRouter input as rawMethods or something
Description
Closes #615 — implements a public REST API for SplitPro using
@trpc/openapi, withOpenAPI 3.1 spec generation, interactive documentation (Scalar), and offset-based
pagination.
API surface (
/api/v1/*)All endpoints authenticate via API key:
Authorization: Bearer spro_<key>.user.meuser.getFriendsuser.getOwnExpensesuser.getBalancesWithFrienduser.getFriendgroup.getAllGroupsgroup.getGroupDetailsexpense.getBalancesexpense.getAllExpensesexpense.getExpensesWithFriendexpense.getGroupExpensesexpense.getExpenseDetailsexpense.addOrEditExpenseInput formats
All GET endpoints accept three equivalent input formats:
?groupId=150&limit=20?input={"groupId":150}?input={"json":{"groupId":150}}POST bodies are accepted as plain JSON (the API transformer handles superjson
transparently for both directions).
Pagination
Four list endpoints use offset-based pagination:
?limit=20&offset=0(defaults:limit=20,offset=0, maxlimit=100){ items: [...], pagination: { total, limit, offset, hasMore } }OpenAPI docs
/api/docs/api/openapi.jsonpnpm gen:openapiArchitecture
src/server/api/apiRouter.ts— curated public API router (separate fromappRouter)src/pages/api/v1/[trpc].ts— handler with API-key context + query param bridgesrc/server/api/pagination.ts— shared offset-pagination helpersscripts/generate-openapi.ts— build-time spec generation + post-processing (injects defaults)appRouterandapiRouterDemo
Checklist
CONTRIBUTING.mdin its entirety