├── .prettierignore ├── .husky ├── pre-push ├── pre-commit └── commit-msg ├── .lintstagedrc ├── .prettierrc ├── src ├── db │ ├── index.ts │ ├── instance.ts │ ├── User.ts │ └── Batch.ts ├── utils │ ├── sleep │ │ └── index.ts │ ├── types │ │ ├── index.ts │ │ └── process-env.ts │ ├── version │ │ ├── VersionInfo.ts │ │ └── index.ts │ ├── index.ts │ ├── mocks │ │ ├── mockBatches.ts │ │ └── mockUsers.ts │ └── homePage.ts ├── trpc │ ├── router │ │ ├── batchRouter.ts │ │ ├── index.ts │ │ ├── utilRouter.ts │ │ └── userRouter.ts │ └── index.ts └── index.ts ├── .gitignore ├── .commitlintrc.ts ├── trpc-api-export ├── builder │ ├── tsup.config.ts │ ├── index.ts │ └── tsconfig.build.json └── dist │ ├── index.js │ └── index.d.ts ├── .github └── workflows │ ├── actions │ └── setup_node_npm │ │ └── action.yml │ └── main.yml ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── eslint.config.mjs /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | build -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run trpc-api-export -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-staged-husky -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*": ["prettier --write --ignore-unknown"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from './instance'; 2 | export * from './Batch'; 3 | export * from './User'; 4 | -------------------------------------------------------------------------------- /src/utils/sleep/index.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); 2 | -------------------------------------------------------------------------------- /src/utils/types/index.ts: -------------------------------------------------------------------------------- 1 | export type DeepMutable = { -readonly [P in keyof TSourceType]: DeepMutable }; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | compiled 5 | 6 | .DS_Store 7 | *.log 8 | .vscode 9 | .idea 10 | 11 | npm-debug.log* 12 | 13 | *.env 14 | -------------------------------------------------------------------------------- /src/utils/version/VersionInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Do not edit this file! Values will be replaced at build time. 3 | */ 4 | export const VersionInfo = { 5 | tag: '', 6 | branch: '', 7 | commit: '', 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mocks/mockBatches'; 2 | export * from './mocks/mockUsers'; 3 | export * from './sleep'; 4 | export * from './types'; 5 | export * from './version'; 6 | export * from './homePage'; 7 | -------------------------------------------------------------------------------- /.commitlintrc.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from '@commitlint/types'; 2 | 3 | const commitlintConfig: UserConfig = { 4 | extends: ['@commitlint/config-conventional'], 5 | }; 6 | 7 | // eslint-disable-next-line import-x/no-default-export 8 | export default commitlintConfig; 9 | -------------------------------------------------------------------------------- /src/trpc/router/batchRouter.ts: -------------------------------------------------------------------------------- 1 | import { db } from 'db'; 2 | import { sleep } from 'utils'; 3 | 4 | import { router, publicProcedure } from '..'; 5 | 6 | export const batchRouter = router({ 7 | list: publicProcedure.query(async () => { 8 | await sleep(1000); 9 | 10 | return db.batches; 11 | }), 12 | }); 13 | -------------------------------------------------------------------------------- /src/db/instance.ts: -------------------------------------------------------------------------------- 1 | import { DeepMutable, mockBatches, mockUsers } from 'utils'; 2 | 3 | import { Batches } from './Batch'; 4 | import { Users } from './User'; 5 | 6 | export const db = { 7 | users: structuredClone(mockUsers) as unknown as DeepMutable, 8 | batches: structuredClone(mockBatches) as unknown as DeepMutable, 9 | }; 10 | -------------------------------------------------------------------------------- /src/trpc/router/index.ts: -------------------------------------------------------------------------------- 1 | import { router } from '..'; 2 | 3 | import { batchRouter } from './batchRouter'; 4 | import { userRouter } from './userRouter'; 5 | import { utilRouter } from './utilRouter'; 6 | 7 | export const appRouter = router({ 8 | batch: batchRouter, 9 | user: userRouter, 10 | util: utilRouter, 11 | }); 12 | 13 | export type AppRouter = typeof appRouter; 14 | -------------------------------------------------------------------------------- /trpc-api-export/builder/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | const tsupConfig = defineConfig({ 4 | entry: ['trpc-api-export/builder/index.ts'], 5 | outDir: 'trpc-api-export/dist', 6 | format: ['esm'], 7 | clean: true, 8 | dts: true, 9 | tsconfig: 'trpc-api-export/builder/tsconfig.build.json', 10 | }); 11 | 12 | // eslint-disable-next-line 13 | export default tsupConfig; 14 | -------------------------------------------------------------------------------- /.github/workflows/actions/setup_node_npm/action.yml: -------------------------------------------------------------------------------- 1 | name: 'setup_node_npm' 2 | description: 'Setup Node/npm ⚙️' 3 | 4 | runs: 5 | using: 'composite' 6 | steps: 7 | - name: Setup Node.js & Cache dependencies 8 | uses: actions/setup-node@v4 9 | with: 10 | node-version: '24.x' 11 | cache: 'npm' 12 | 13 | - name: Install dependencies 14 | shell: bash 15 | run: npm ci 16 | -------------------------------------------------------------------------------- /src/utils/version/index.ts: -------------------------------------------------------------------------------- 1 | import { VersionInfo } from './VersionInfo'; 2 | 3 | type GetVersionInfo = () => string; 4 | 5 | export const getVersionInfo: GetVersionInfo = () => { 6 | if (VersionInfo.tag !== '') { 7 | return VersionInfo.tag; 8 | } 9 | 10 | if (VersionInfo.commit !== '') { 11 | return `${VersionInfo.branch} [${VersionInfo.commit.substring(0, 7)}]`; 12 | } 13 | 14 | return 'Local build'; 15 | }; 16 | -------------------------------------------------------------------------------- /src/trpc/index.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server'; 2 | import * as trpcExpress from '@trpc/server/adapters/express'; 3 | import superjson from 'superjson'; 4 | 5 | export const createContext = ({ req, res }: trpcExpress.CreateExpressContextOptions) => ({ req, res }); 6 | const t = initTRPC.create({ transformer: superjson }); 7 | 8 | export const middleware = t.middleware; 9 | export const router = t.router; 10 | export const publicProcedure = t.procedure; 11 | -------------------------------------------------------------------------------- /src/db/User.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const USER_ROLES = ['administrator', 'apprentice', 'standard'] as const; 4 | 5 | export const UserConfigSchema = z.object({ 6 | id: z.string(), 7 | email: z.string(), 8 | name: z.string(), 9 | username: z.string(), 10 | imageUrl: z.string().optional(), 11 | role: z.enum(USER_ROLES), 12 | }); 13 | 14 | export type User = z.infer; 15 | 16 | export type Users = ReadonlyArray; 17 | -------------------------------------------------------------------------------- /src/trpc/router/utilRouter.ts: -------------------------------------------------------------------------------- 1 | import { db, Batches, Users } from 'db'; 2 | import { DeepMutable, mockBatches, mockUsers } from 'utils'; 3 | 4 | import { router, publicProcedure } from '..'; 5 | 6 | export const utilRouter = router({ 7 | seedDb: publicProcedure.mutation(() => { 8 | db.users = structuredClone(mockUsers) as unknown as DeepMutable; 9 | db.batches = structuredClone(mockBatches) as unknown as DeepMutable; 10 | 11 | return 'Database initialized successfully.' as const; 12 | }), 13 | }); 14 | -------------------------------------------------------------------------------- /trpc-api-export/dist/index.js: -------------------------------------------------------------------------------- 1 | // src/db/User.ts 2 | import { z } from 'zod'; 3 | var USER_ROLES = ['administrator', 'apprentice', 'standard']; 4 | var UserConfigSchema = z.object({ 5 | id: z.string(), 6 | email: z.string(), 7 | name: z.string(), 8 | username: z.string(), 9 | imageUrl: z.string().optional(), 10 | role: z.enum(USER_ROLES), 11 | }); 12 | 13 | // trpc-api-export/builder/index.ts 14 | var SharedSquareObject = { 15 | shape: 'square', 16 | size: 50, 17 | }; 18 | export { SharedSquareObject, USER_ROLES }; 19 | -------------------------------------------------------------------------------- /src/utils/types/process-env.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-object-type */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /* eslint-disable @typescript-eslint/no-namespace */ 4 | /* eslint-disable @typescript-eslint/consistent-type-definitions */ 5 | 6 | import { z } from 'zod'; 7 | 8 | const envVariables = z.object({ 9 | NODE_ENV: z.union([z.literal('development'), z.literal('production')]), 10 | PORT: z.string().optional(), 11 | }); 12 | 13 | declare global { 14 | namespace NodeJS { 15 | interface ProcessEnv extends z.infer {} 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/db/Batch.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { UserConfigSchema } from './User'; 4 | 5 | export const BatchConfigSchema = z.object({ 6 | id: z.string(), 7 | title: z.string(), 8 | description: z.string().nullable(), 9 | purity: z.number().min(0).max(100), 10 | weight: z.number().positive({ message: 'Must be a positive number.' }), 11 | producers: UserConfigSchema.array(), 12 | supplier: z.object({ id: z.string(), name: z.string(), description: z.string().nullable() }).nullable(), 13 | }); 14 | 15 | type Batch = z.infer; 16 | 17 | export type Batches = ReadonlyArray; 18 | -------------------------------------------------------------------------------- /trpc-api-export/builder/index.ts: -------------------------------------------------------------------------------- 1 | export type { AppRouter } from 'trpc/router'; 2 | 3 | // Export user roles array as source of truth for frontend (select component, render list of available roles etc.) 4 | export { USER_ROLES } from 'db/User'; 5 | 6 | // Export any backend type, object, array etc. that should be shared with frontend 7 | type Square = { 8 | shape: 'square'; 9 | size: number; 10 | }; 11 | type Rectangle = { 12 | shape: 'rectangle'; 13 | width: number; 14 | height: number; 15 | }; 16 | export type Shape = Square | Rectangle; 17 | 18 | export const SharedSquareObject: Shape = { 19 | shape: 'square', 20 | size: 50, 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "baseUrl": "src", 6 | "paths": { 7 | ".": ["."] 8 | }, 9 | "allowJs": true, 10 | "strict": true, 11 | "strictNullChecks": true, 12 | "pretty": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "incremental": true, 17 | "skipLibCheck": true, 18 | "esModuleInterop": true, 19 | "module": "ESNext", 20 | "moduleResolution": "Bundler", 21 | "target": "ES2022", 22 | "moduleDetection": "force" 23 | }, 24 | "include": [".", ".eslintrc.cjs", ".commitlintrc.ts"], 25 | "exclude": ["build", "node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as trpcExpress from '@trpc/server/adapters/express'; 2 | import cors from 'cors'; 3 | import express from 'express'; 4 | 5 | import { homePage } from 'utils'; 6 | 7 | import { createContext } from './trpc'; 8 | import { appRouter } from './trpc/router'; 9 | 10 | const app = express(); 11 | 12 | app.use(cors()); 13 | 14 | app.use( 15 | '/trpc', 16 | trpcExpress.createExpressMiddleware({ 17 | router: appRouter, 18 | createContext, 19 | }), 20 | ); 21 | 22 | // @ts-expect-error Upgrade express to v5 23 | app.use('/', (_req, res) => { 24 | return res.type('html').send(homePage); 25 | }); 26 | 27 | const PORT = process.env.PORT ?? 4000; 28 | 29 | app.listen(PORT, () => { 30 | console.log(`Server running on http://localhost:${PORT}`); 31 | }); 32 | -------------------------------------------------------------------------------- /trpc-api-export/builder/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Export tRPC API", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationDir": "dist", 7 | "baseUrl": "../../src", 8 | "paths": { 9 | ".": ["."] 10 | }, 11 | "allowJs": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "pretty": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "skipLibCheck": true, 19 | "esModuleInterop": true, 20 | "module": "ESNext", 21 | "moduleResolution": "Bundler", 22 | "target": "ES2022", 23 | "moduleDetection": "force" 24 | }, 25 | "exclude": ["../../build", "../../node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mkosir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/trpc/router/userRouter.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { db, User, UserConfigSchema } from 'db'; 4 | import { sleep } from 'utils'; 5 | 6 | import { router, publicProcedure } from '../'; 7 | 8 | export const userRouter = router({ 9 | list: publicProcedure.query(async () => { 10 | await sleep(1000); 11 | 12 | return db.users; 13 | }), 14 | 15 | show: publicProcedure.input(z.string().min(1)).query(async ({ input: userId }) => { 16 | await sleep(1000); 17 | 18 | const user = db.users.find((user) => user.id === userId); 19 | 20 | if (user) { 21 | return user; 22 | } 23 | 24 | return `User with id:${userId} does not exist in database.` as const; 25 | }), 26 | 27 | destroy: publicProcedure.input(z.object({ id: z.string().min(1) })).mutation(async ({ input: { id } }) => { 28 | await sleep(1000); 29 | 30 | const index = db.users.findIndex((user) => user.id === id); 31 | const deletedUser = db.users.splice(index, 1); 32 | 33 | return deletedUser; 34 | }), 35 | 36 | create: publicProcedure 37 | .input(z.object({ user: UserConfigSchema.omit({ id: true }) })) 38 | .mutation(async ({ input: { user } }) => { 39 | await sleep(1000); 40 | 41 | const newUser: User = { id: crypto.randomUUID(), ...user }; 42 | 43 | db.users.push(newUser); 44 | 45 | return newUser; 46 | }), 47 | }); 48 | -------------------------------------------------------------------------------- /src/utils/mocks/mockBatches.ts: -------------------------------------------------------------------------------- 1 | import { Batches } from 'db'; 2 | 3 | import { mockUsers } from './mockUsers'; 4 | 5 | export const mockBatches = [ 6 | { 7 | id: '2f008731-4645-43de-8af9-3060d4086001', 8 | title: 'Blue Sky', 9 | description: 'summer batch', 10 | purity: 99.11, 11 | weight: 145.64, 12 | producers: [mockUsers[0], mockUsers[1]], 13 | supplier: { 14 | id: '3f008731-4645-43de-8af9-3060d4086001', 15 | name: 'Golden Moth Chemical', 16 | description: 'Golden bee company logo.', 17 | }, 18 | }, 19 | { 20 | id: '2f008731-4645-43de-8af9-3060d4086002', 21 | title: 'Blue Sky', 22 | description: 'bad batch', 23 | purity: 45.72, 24 | weight: 142.18, 25 | producers: [mockUsers[0], mockUsers[1]], 26 | supplier: { 27 | id: '3f008731-4645-43de-8af9-3060d4086002', 28 | name: 'Warehouse', 29 | description: 30 | "Lock security guard into portable toilet, don't roll but carry stolen barrel of methylamine, improvise as it goes...", 31 | }, 32 | }, 33 | { 34 | id: '2f008731-4645-43de-8af9-3060d4086003', 35 | title: 'Blue Sky', 36 | description: null, 37 | purity: 99.4, 38 | weight: 149.7, 39 | producers: [mockUsers[0]], 40 | supplier: null, 41 | }, 42 | { 43 | id: '2f008731-4645-43de-8af9-3060d4086004', 44 | title: 'Blue Sky', 45 | description: null, 46 | purity: 98.64, 47 | weight: 146.51, 48 | producers: [mockUsers[1]], 49 | supplier: null, 50 | }, 51 | ] satisfies Batches; 52 | -------------------------------------------------------------------------------- /src/utils/homePage.ts: -------------------------------------------------------------------------------- 1 | import { getVersionInfo } from './version'; 2 | 3 | export const homePage = ` 4 | 5 | 6 | 7 | tRPC API Boilerplate 8 | 9 | 10 | 11 |

tRPC API Boilerplate

12 |
${getVersionInfo()}
13 |
14 |
15 | Router: 16 |
    17 | User 18 |
  • List
  • 19 |
20 |
    21 | Batch 22 |
  • List
  • 23 |
24 |
    25 | Util 26 |
  • 27 | 28 | 29 |
  • 30 |
31 |
32 | 47 | 48 | 49 | `; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trpc-api-boilerplate", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "main": "./trpc-api-export/dist/index.js", 6 | "exports": "./trpc-api-export/dist/index.js", 7 | "types": "./trpc-api-export/dist/index.d.ts", 8 | "sideEffects": false, 9 | "files": [ 10 | "trpc-api-export/dist", 11 | "README.md" 12 | ], 13 | "scripts": { 14 | "trpc-api-export": "tsup --config trpc-api-export/builder/tsup.config.ts && npm run format-fix", 15 | "prepare": "husky", 16 | "build": "rm -rf build && tsc -p tsconfig.json", 17 | "dev": "tsx watch src/index.ts", 18 | "start": "tsx src/index.ts", 19 | "lint": "eslint --report-unused-disable-directives --max-warnings 0 .", 20 | "lint-fix": "eslint --fix .", 21 | "lint-staged-husky": "lint-staged", 22 | "tsc": "tsc -p tsconfig.json", 23 | "format-lint": "prettier --config .prettierrc --check --ignore-unknown .", 24 | "format-fix": "prettier --config .prettierrc --write --ignore-unknown -l .", 25 | "release": "semantic-release --branches main" 26 | }, 27 | "dependencies": { 28 | "@trpc/server": "11.4.3", 29 | "cors": "2.8.5", 30 | "express": "5.1.0", 31 | "superjson": "2.2.2", 32 | "zod": "3.25.67" 33 | }, 34 | "devDependencies": { 35 | "@commitlint/cli": "19.8.1", 36 | "@commitlint/config-conventional": "19.8.1", 37 | "@eslint/js": "9.30.1", 38 | "@types/cors": "2.8.19", 39 | "@types/eslint": "9.6.1", 40 | "@types/express": "5.0.3", 41 | "eslint": "9.30.1", 42 | "eslint-config-prettier": "10.1.5", 43 | "eslint-import-resolver-typescript": "4.4.4", 44 | "eslint-plugin-import-x": "4.16.1", 45 | "eslint-plugin-prettier": "5.5.1", 46 | "husky": "9.1.7", 47 | "lint-staged": "16.1.2", 48 | "prettier": "3.6.2", 49 | "semantic-release": "24.2.6", 50 | "tsup": "8.5.0", 51 | "tsx": "4.20.3", 52 | "typescript": "5.8.3", 53 | "typescript-eslint": "8.35.1" 54 | }, 55 | "engines": { 56 | "node": ">=24.3.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tRPC API Boilerplate ![Heisenberg](https://raw.githubusercontent.com/mkosir/trpc-fe-boilerplate-vite/main/misc/heisenberg_75.png) 2 | 3 | [![CI][ci-badge]][ci-url] 4 | 5 | Minimal [tRPC](https://trpc.io/) API boilerplate for projects with separate backend and frontend repositories. Easily publish fully typesafe APIs that any frontend can consume. 6 | 7 | Monorepos are great, but sometimes the architecture requires separating the backend and frontend into distinct repositories. 8 | 9 | ### Why use this? 10 | 11 | This boilerplate is ideal when you want to separate: 12 | 13 | - **Domain/business logic** – expose only what needs to be exposed through the API. 14 | - **Developer responsibilities** – for larger teams/companies. 15 | - **CI/CD pipelines** – manage PRs, issues, and deployments independently. 16 | 17 | ## Running 18 | 19 | _Easily set up a local development environment_ 20 | 21 | - Fork & clone the repo 22 | - Run `npm install` 23 | - Make changes to the tRPC API 24 | - Push - a new [package](https://www.npmjs.com/package/trpc-api-boilerplate) is released 📦 [![npm version][npm-badge]][npm-url] 25 | - In your frontend app, install it `npm install trpc-api-boilerplate` 26 | 27 | ## Example Repos 28 | 29 | Example frontend app repositories: 30 | 31 | - [tRPC Frontend Boilerplate](https://github.com/mkosir/trpc-fe-boilerplate-vite) - Vite 32 | - [tRPC Frontend Boilerplate](https://github.com/mkosir/trpc-fe-boilerplate-next) - Next.js 33 | 34 | ### Avoid publishing the package? 35 | 36 | If for whatever reason publishing a package is not an option: 37 | 38 | - privacy concerns 39 | - faster development iterations - skip CI 40 | - ... 41 | 42 | Use the repository to share types by running `npm run trpc-api-export` and push code changes. 43 | In your [frontend app](https://github.com/mkosir/trpc-fe-boilerplate-vite/blob/main/package.json#L7), consume types by running `npm run trpc-api-import`. 44 | 45 | 46 | 47 | [ci-badge]: https://github.com/mkosir/trpc-api-boilerplate/actions/workflows/main.yml/badge.svg 48 | [ci-url]: https://github.com/mkosir/trpc-api-boilerplate/actions/workflows/main.yml 49 | [npm-url]: https://www.npmjs.com/package/trpc-api-boilerplate 50 | [npm-badge]: https://img.shields.io/npm/v/trpc-api-boilerplate.svg 51 | -------------------------------------------------------------------------------- /src/utils/mocks/mockUsers.ts: -------------------------------------------------------------------------------- 1 | import { Users } from 'db'; 2 | 3 | const baseImageUrl = 'https://raw.githubusercontent.com/mkosir/trpc-fe-boilerplate-vite/main/misc/user-images'; 4 | 5 | export const mockUsers = [ 6 | { 7 | id: '1f008731-4645-43de-8af9-3060d4086001', 8 | email: 'walter.white@mail.com', 9 | name: 'Walter White', 10 | username: 'Heisenberg', 11 | imageUrl: `${baseImageUrl}/heisenberg.jpg`, 12 | role: 'administrator', 13 | }, 14 | { 15 | id: '1f008731-4645-43de-8af9-3060d4086002', 16 | email: 'jesse.pinkman@mail.com', 17 | name: 'Jesse Pinkman', 18 | username: 'Jesse', 19 | imageUrl: `${baseImageUrl}/jesse.jpg`, 20 | role: 'apprentice', 21 | }, 22 | { 23 | id: '1f008731-4645-43de-8af9-3060d4086003', 24 | email: 'skyler.white@mail.com', 25 | name: 'Skyler White', 26 | username: 'Sky', 27 | imageUrl: `${baseImageUrl}/sky.jpg`, 28 | role: 'standard', 29 | }, 30 | { 31 | id: '1f008731-4645-43de-8af9-3060d4086004', 32 | email: 'hank.schrader@mail.com', 33 | name: 'Hank Schrader', 34 | username: 'Hank', 35 | imageUrl: `${baseImageUrl}/hank.jpg`, 36 | role: 'standard', 37 | }, 38 | { 39 | id: '1f008731-4645-43de-8af9-3060d4086005', 40 | email: 'marie.schrader@mail.com', 41 | name: 'Marie Schrader', 42 | username: 'Marie', 43 | role: 'standard', 44 | }, 45 | { 46 | id: '1f008731-4645-43de-8af9-3060d4086006', 47 | email: 'saul.goodman@mail.com', 48 | name: 'Saul Goodman', 49 | username: 'Jimmy', 50 | imageUrl: `${baseImageUrl}/jimmy.jpg`, 51 | role: 'standard', 52 | }, 53 | { 54 | id: '1f008731-4645-43de-8af9-3060d4086007', 55 | email: 'gustavo.fring@mail.com', 56 | name: 'Gustavo Fring', 57 | username: 'Gus', 58 | imageUrl: `${baseImageUrl}/gus.jpg`, 59 | role: 'standard', 60 | }, 61 | { 62 | id: '1f008731-4645-43de-8af9-3060d4086008', 63 | email: 'michael.ehrmantraut@mail.com', 64 | name: 'Michael Ehrmantraut', 65 | username: 'Mike', 66 | imageUrl: `${baseImageUrl}/mike.jpg`, 67 | role: 'standard', 68 | }, 69 | { 70 | id: '1f008731-4645-43de-8af9-3060d4086009', 71 | email: 'hector.salamanca@mail.com', 72 | name: 'Hector Salamanca', 73 | username: 'Tio', 74 | imageUrl: `${baseImageUrl}/tio.jpg`, 75 | role: 'standard', 76 | }, 77 | { 78 | id: '1f008731-4645-43de-8af9-3060d4086010', 79 | email: 'alberto.salamanca@mail.com', 80 | name: 'Alberto Salamanca', 81 | username: 'Tuco', 82 | role: 'standard', 83 | }, 84 | { 85 | id: '1f008731-4645-43de-8af9-3060d4086011', 86 | email: 'gale.boetticher@mail.com', 87 | name: 'Gale Boetticher', 88 | username: 'Captain Nerd', 89 | role: 'apprentice', 90 | }, 91 | ] as const satisfies Users; 92 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | FILE_PATH_VERSION_INFO: ./src/utils/version/VersionInfo.ts 10 | 11 | jobs: 12 | build: 13 | name: Build 🏗️ 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 🛎️ 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Node/npm ⚙️ 21 | uses: ./.github/workflows/actions/setup_node_npm 22 | 23 | - name: Version Info 💉 24 | run: | 25 | COMMIT=$GITHUB_SHA 26 | BRANCH=${GITHUB_REF#refs/heads/} 27 | TAG='' 28 | if [[ ${{ github.ref_type }} == 'tag' ]]; then 29 | TAG=${GITHUB_REF#refs/tags/} 30 | fi 31 | echo "write tag, branch & commit id to $FILE_PATH_VERSION_INFO" 32 | echo "export const VersionInfo = { tag: '$TAG', branch: '$BRANCH', commit: '$COMMIT' };" > $FILE_PATH_VERSION_INFO 33 | cat $FILE_PATH_VERSION_INFO 34 | 35 | - name: Build 🏗️ 36 | run: npm run build 37 | 38 | lint: 39 | name: Lint ✅ 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - name: Checkout 🛎️ 44 | uses: actions/checkout@v4 45 | 46 | - name: Setup Node/npm ⚙️ 47 | uses: ./.github/workflows/actions/setup_node_npm 48 | 49 | - name: Lint ✅ 50 | run: npm run lint 51 | 52 | tsc: 53 | name: TypeScript Compiler 🔎 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - name: Checkout 🛎️ 58 | uses: actions/checkout@v4 59 | 60 | - name: Setup Node/npm ⚙️ 61 | uses: ./.github/workflows/actions/setup_node_npm 62 | 63 | - name: TypeScript Compiler 🔎 64 | run: npm run tsc 65 | 66 | release-npm: 67 | name: Release npm 📦 68 | needs: [build, lint, tsc] 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | - name: Checkout 🛎️ 73 | uses: actions/checkout@v4 74 | 75 | - name: Setup Node/npm ⚙️ 76 | uses: ./.github/workflows/actions/setup_node_npm 77 | 78 | - name: Export tRPC API 🏗️ 79 | run: npm run trpc-api-export 80 | 81 | - name: Release npm 📦 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 85 | run: npm run release 86 | 87 | deploy: 88 | name: Deploy 🚀 89 | needs: [build, lint, tsc] 90 | runs-on: ubuntu-latest 91 | 92 | environment: 93 | name: tRPC API 94 | url: https://trpc-api-boilerplate.onrender.com 95 | 96 | steps: 97 | - name: Deploy 🚀 98 | uses: fjogeleit/http-request-action@v1 99 | with: 100 | url: ${{ secrets.RENDER_DEPLOY_HOOK }} 101 | method: 'GET' 102 | timeout: 20000 103 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import eslintConfigPrettier from 'eslint-config-prettier'; 3 | import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; 4 | import * as eslintPluginImportX from 'eslint-plugin-import-x'; 5 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 6 | import * as tseslint from 'typescript-eslint'; 7 | 8 | export default tseslint.config( 9 | eslint.configs.recommended, 10 | eslintPluginImportX.flatConfigs.recommended, 11 | eslintPluginImportX.flatConfigs.typescript, 12 | ...tseslint.configs.strictTypeChecked, 13 | ...tseslint.configs.stylisticTypeChecked, 14 | eslintPluginPrettierRecommended, 15 | eslintConfigPrettier, 16 | { 17 | ignores: ['!.*', 'node_modules', 'dist', 'trpc-api-export/dist', 'compiled', 'build'], 18 | }, 19 | { 20 | languageOptions: { 21 | parserOptions: { 22 | projectService: true, 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 24 | tsconfigRootDir: import.meta.name, 25 | }, 26 | }, 27 | settings: { 28 | 'import-x/resolver-next': [createTypeScriptImportResolver()], 29 | react: { version: 'detect' }, 30 | }, 31 | }, 32 | { 33 | files: ['**/*.{js,ts,tsx}'], 34 | 35 | rules: { 36 | 'prefer-template': 'error', 37 | 'no-nested-ternary': 'error', 38 | 'no-unneeded-ternary': 'error', 39 | 40 | '@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }], 41 | '@typescript-eslint/consistent-type-definitions': ['error', 'type'], 42 | '@typescript-eslint/array-type': ['error', { default: 'generic' }], 43 | '@typescript-eslint/prefer-nullish-coalescing': 'error', 44 | '@typescript-eslint/no-unnecessary-condition': 'error', 45 | '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], 46 | '@typescript-eslint/restrict-plus-operands': 'off', 47 | '@typescript-eslint/restrict-template-expressions': 'off', 48 | '@typescript-eslint/naming-convention': [ 49 | 'error', 50 | { 51 | selector: 'typeAlias', 52 | format: ['PascalCase'], 53 | }, 54 | { 55 | selector: 'variable', 56 | types: ['boolean'], 57 | format: ['PascalCase'], 58 | prefix: ['is', 'should', 'has', 'can', 'did', 'will'], 59 | }, 60 | { 61 | // Generic type parameter must start with letter T, followed by any uppercase letter. 62 | selector: 'typeParameter', 63 | format: ['PascalCase'], 64 | custom: { regex: '^T[A-Z]', match: true }, 65 | }, 66 | ], 67 | 68 | 'import-x/no-default-export': 'error', 69 | 'import-x/order': [ 70 | 'error', 71 | { 72 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling'], 73 | 'newlines-between': 'always', 74 | alphabetize: { 75 | order: 'asc', 76 | caseInsensitive: true, 77 | }, 78 | }, 79 | ], 80 | }, 81 | }, 82 | ); 83 | -------------------------------------------------------------------------------- /trpc-api-export/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as _trpc_server from '@trpc/server'; 2 | 3 | type DeepMutable = { 4 | -readonly [P in keyof TSourceType]: DeepMutable; 5 | }; 6 | 7 | declare const appRouter: _trpc_server.TRPCBuiltRouter< 8 | { 9 | ctx: object; 10 | meta: object; 11 | errorShape: _trpc_server.TRPCDefaultErrorShape; 12 | transformer: true; 13 | }, 14 | _trpc_server.TRPCDecorateCreateRouterOptions<{ 15 | batch: _trpc_server.TRPCBuiltRouter< 16 | { 17 | ctx: object; 18 | meta: object; 19 | errorShape: _trpc_server.TRPCDefaultErrorShape; 20 | transformer: true; 21 | }, 22 | _trpc_server.TRPCDecorateCreateRouterOptions<{ 23 | list: _trpc_server.TRPCQueryProcedure<{ 24 | input: void; 25 | output: DeepMutable<{ 26 | id: string; 27 | title: string; 28 | description: string | null; 29 | purity: number; 30 | weight: number; 31 | producers: { 32 | id: string; 33 | email: string; 34 | name: string; 35 | username: string; 36 | role: 'administrator' | 'apprentice' | 'standard'; 37 | imageUrl?: string | undefined; 38 | }[]; 39 | supplier: { 40 | id: string; 41 | name: string; 42 | description: string | null; 43 | } | null; 44 | }>[]; 45 | meta: object; 46 | }>; 47 | }> 48 | >; 49 | user: _trpc_server.TRPCBuiltRouter< 50 | { 51 | ctx: object; 52 | meta: object; 53 | errorShape: _trpc_server.TRPCDefaultErrorShape; 54 | transformer: true; 55 | }, 56 | _trpc_server.TRPCDecorateCreateRouterOptions<{ 57 | list: _trpc_server.TRPCQueryProcedure<{ 58 | input: void; 59 | output: DeepMutable<{ 60 | id: string; 61 | email: string; 62 | name: string; 63 | username: string; 64 | role: 'administrator' | 'apprentice' | 'standard'; 65 | imageUrl?: string | undefined; 66 | }>[]; 67 | meta: object; 68 | }>; 69 | show: _trpc_server.TRPCQueryProcedure<{ 70 | input: string; 71 | output: 72 | | DeepMutable<{ 73 | id: string; 74 | email: string; 75 | name: string; 76 | username: string; 77 | role: 'administrator' | 'apprentice' | 'standard'; 78 | imageUrl?: string | undefined; 79 | }> 80 | | `User with id:${string} does not exist in database.`; 81 | meta: object; 82 | }>; 83 | destroy: _trpc_server.TRPCMutationProcedure<{ 84 | input: { 85 | id: string; 86 | }; 87 | output: DeepMutable<{ 88 | id: string; 89 | email: string; 90 | name: string; 91 | username: string; 92 | role: 'administrator' | 'apprentice' | 'standard'; 93 | imageUrl?: string | undefined; 94 | }>[]; 95 | meta: object; 96 | }>; 97 | create: _trpc_server.TRPCMutationProcedure<{ 98 | input: { 99 | user: { 100 | email: string; 101 | name: string; 102 | username: string; 103 | role: 'administrator' | 'apprentice' | 'standard'; 104 | imageUrl?: string | undefined; 105 | }; 106 | }; 107 | output: { 108 | id: string; 109 | email: string; 110 | name: string; 111 | username: string; 112 | role: 'administrator' | 'apprentice' | 'standard'; 113 | imageUrl?: string | undefined; 114 | }; 115 | meta: object; 116 | }>; 117 | }> 118 | >; 119 | util: _trpc_server.TRPCBuiltRouter< 120 | { 121 | ctx: object; 122 | meta: object; 123 | errorShape: _trpc_server.TRPCDefaultErrorShape; 124 | transformer: true; 125 | }, 126 | _trpc_server.TRPCDecorateCreateRouterOptions<{ 127 | seedDb: _trpc_server.TRPCMutationProcedure<{ 128 | input: void; 129 | output: 'Database initialized successfully.'; 130 | meta: object; 131 | }>; 132 | }> 133 | >; 134 | }> 135 | >; 136 | type AppRouter = typeof appRouter; 137 | 138 | declare const USER_ROLES: readonly ['administrator', 'apprentice', 'standard']; 139 | 140 | type Square = { 141 | shape: 'square'; 142 | size: number; 143 | }; 144 | type Rectangle = { 145 | shape: 'rectangle'; 146 | width: number; 147 | height: number; 148 | }; 149 | type Shape = Square | Rectangle; 150 | declare const SharedSquareObject: Shape; 151 | 152 | export { type AppRouter, type Shape, SharedSquareObject, USER_ROLES }; 153 | --------------------------------------------------------------------------------