cd ~/Projects/Personal
mkdir trpc-demo
cd trpc-demo
yarn init -p -y
mkdir backend
cd backend
yarn init -p -y
yarn add -D typescript
yarn tsc --init
yarn add express cors
yarn add -D @types/express @types/cors
yarn add -D tsx
cd ../
yarn create vite
cd frontend
yarn
code ../
import express from "express"
import cors from "cors"
const router = express()
router.use(cors())
router.get("/", (req, res) => {
res.send("Hello World")
})
router.listen(5555, () => console.log("Server is running http://localhost:5555"))
"scripts": {
"dev": "NODE_PATH=./src yarn tsx watch src/server.ts",
"start": "NODE_PATH=./dist node dist/server.js",
"build": "yarn tsc"
}
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml"
href="https://secure.meetupstatic.com/photos/event/9/f/2/d/clean_469240749.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>San Diego + TS</title>
</head>
<div>
<a href='https://sandiegojs.org' target='_blank'>
<img
src='https://secure.meetupstatic.com/photos/event/9/f/2/d/clean_469240749.webp'
className='logo'
alt='SanDiego TS logo'
/>
</a>
</div>
<h2>San Diego TypeScript Community</h2>
"scripts": {
"dev:frontend": "cd frontend && yarn dev",
"dev:backend": "cd backend && yarn dev",
"dev": "yarn dev:frontend & yarn dev:backend"
}
export type SharedInput = {
name: string
count: number
}
export const backendFunction = (shared: SharedInput) => {
console.log("Backend function", shared)
return { message: `Hi ${shared.name}, Remotely Yours, Backend.`, count: shared.count }
}
const response = backendFunction({ name: "Stasi", count })
"references": [{ "path": "../backend/tsconfig.json" }],
"paths": {
"@backend/*": ["../backend/src/*"]
}
import tsconfigPaths from "vite-tsconfig-paths"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [tsconfigPaths(), react()],
})
router.get("/shared", (req, res) => {
const response = backendFunction(req.query as any)
res.json(response)
})
Update backend/src/shared.ts
import z from "zod"
export const sharedSchema = z.object({
name: z.coerce.string(),
count: z.coerce.number(),
})
export type SharedInput = z.infer<typeof sharedSchema>
export type SharedOutput = ReturnType<typeof backendFunction>
// use sharedSchema on server
const useSharedApi = (count: number) => {
const [apiResponse, setApiResponse] = useState<SharedOutput | null>(null)
const fetchShared = async () => {
const data = sharedSchema.parse({ name: "Frontend", count })
const query = new URLSearchParams({ name: data.name, count: data.count.toString() }).toString()
const response = await fetch(`http://localhost:5555/shared?${query.toString()}`)
const responseData = await response.json()
setApiResponse(responseData)
}
useEffect(() => {
void fetchShared()
}, [count])
return apiResponse
}
cd backend
yarn add @trpc/server
import { initTRPC } from "@trpc/server"
import { backendFunction, sharedSchema } from "./shared"
import { createExpressMiddleware } from "@trpc/server/adapters/express"
const trpc = initTRPC.create()
const trpcRouter = trpc.router({
hello: trpc.procedure.input(sharedSchema).query(({ input }) => {
return backendFunction(input)
}),
})
export type TrpcRouter = typeof trpcRouter
export const trpcMiddleware = createExpressMiddleware({ router: trpcRouter })
router.use("/trpc", trpcMiddleware)
cd ../frontend
yarn add @trpc/client
const trpcClient = createTRPCProxyClient<TrpcRouter>({
links: [
httpBatchLink({
url: "http://localhost:5555/trpc",
}),
],
})
// inside useSharedApi
const fetchTrpc = async () => {
const data = await trpcClient.hello.query({ name: "Frontend", count })
setApiResponse(data)
}
input type checking
output type
refactoring
build errors
DEMO 3: TRPC Authentication flow
cd ../backend
yarn add jsonwebtoken
yarn add -D @types/jsonwebtoken
import jwt from "jsonwebtoken"
// spell-checker: disable-next-line
const TOKEN_SECRET = "sandiegojsmeetup" // 16 characters
const ACCESS_EXPIRATION = 5 * 60 * 1000 // 5 minutes
const REFRESH_EXPIRATION = 24 * 60 * 60 * 1000 // 24 hours
export type TokenData = {
name?: string
isAdmin?: boolean
}
const generateToken = (data: TokenData, expiresIn: number) => jwt.sign(data, TOKEN_SECRET, { expiresIn })
export const getTokens = (data: TokenData) => {
return {
...data,
exp: Date.now() + ACCESS_EXPIRATION,
accessToken: generateToken(data, ACCESS_EXPIRATION),
refreshToken: generateToken(data, REFRESH_EXPIRATION),
}
}
export type Session = ReturnType<typeof getTokens>
export const parseToken = (token: string) => {
try {
return jwt.verify(token, TOKEN_SECRET) as TokenData
} catch (error) {
console.error("parse token error", error)
throw new Error("Invalid token. Please login again.")
}
}
authorize: trpc.procedure
.input(
z.object({
name: z.string(),
password: z.string(),
}),
)
.mutation(async ({ input }) => {
const response = getTokens({
name: input.name,
isAdmin: true,
})
return response
}),
Backend TRPC Private router
Frontend TRPC Private router