今天我們要來實作 refetch_token跟 access_token 實作~流程大概會是如下。
> npx create-next-app@latest authorization --ts
這邊使 trpc 團隊推薦的 tsconfig ,筆者可以根據自己偏好調整
//tsconfig.json
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"./*.js",
"./src/**/*.js"
],
"exclude": ["node_modules"]
}
這邊我們的 db 會用 docker 起一個 postgresQL 跟 redis。
// docker-compose.yml
version: '3'
services:
postgres:
image: postgres
container_name: postgres
ports:
- '6500:5432'
restart: always
env_file:
- ./.env
volumes:
- postgres-db:/var/lib/postgresql/data
redis:
image: redis:latest
container_name: redis
ports:
- '6379:6379'
volumes:
- redis:/data
volumes:
postgres-db:
redis:
這邊是本次專案會用到的 env ,以下是範例讀者也可以自行調整。
// .env
DATABASE_PORT=6500
POSTGRES_PASSWORD=password123
POSTGRES_USER=postgres
POSTGRES_DB=trpc-nextjs
POSTGRES_HOST=postgres
POSTGRES_HOSTNAME=127.0.0.1
NEXT_PUBLIC_TRPC_ENDPOINT=""
ACCESS_TOKEN_PRIVATE_KEY=""
ACCESS_TOKEN_PUBLIC_KEY=""
REFRESH_TOKEN_PRIVATE_KEY=""
REFRESH_TOKEN_PUBLIC_KEY=""
接著我們 docker 啟動~
> docker-compose up -d
然後檢查 port 有沒有成功對接。
> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4c9ab62e6c53 postgres "docker-entrypoint.s…" 24 hours ago Up 2 minutes 0.0.0.0:6500->5432/tcp postgres
6544419c75b1 redis:latest "docker-entrypoint.s…" 2 days ago Exited (255) 2 minutes ago 0.0.0.0:6379->6379/tcp redis
> npm install @prisma/client && npm install -D prisma
初始化 prisma
> npx prisma init
然後記得修改一下 DATABASE_URL 的值
// .env
DATABASE_PORT=6500
POSTGRES_PASSWORD=password123
POSTGRES_USER=postgres
POSTGRES_DB=trpc-nextjs
POSTGRES_HOST=postgres
POSTGRES_HOSTNAME=127.0.0.1
DATABASE_URL="postgresql://postgres:password123@localhost:6500/trpc-nexjs?schema=public"
新增 schema ,這邊順便補充一下用到的 decorator 好了。
@@map : 指定 orm 映射到 db 的 table 名稱叫什麼, @@map("users") 代表 User modal 會在 db 中,對應到 users 的 table。@db.VarChar : prisma 有提供一些 db 的 validate 功能例如 VarChar 設定。enum : prisma 可以透過 enum type 列舉出有哪些 value 可以填入。
model User{
@@map("users")
id String @id @default(uuid())
name String @db.VarChar(255)
email String @unique
photo String @default("default.png")
verified Boolean @default(false)
hashPassword String?
role RoleEnumType @default(user)
createAt DateTime @default(now())
updateAt DateTime @updatedAt
provider String?
}
enum RoleEnumType {
user
admin
}
之後我們寫 script 來 migrate
// package.json
{
"scripts": {
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && yarn prisma generate",
"db:push": "npx prisma db push"
},
}
--create-only : 先產生 migrate schema 但還沒應用到 db ,可用於檢視 SQL command 有無錯誤。
>npx prisma migrate dev --create-only
之後再跑一次 npx prisma migrate dev db 就會吃 migrate 的內容了~
>npx prisma migrate dev
之後只要每次到 npm run 就會幫你 migrate schema 然後產生 prisma client 的 type了。
> npm run db:migrate && npm run db:push
透過 docker 有 redis 的 local 環境後,就需要加 client instance 拉~
// src/server/utils/connectRedis.ts
import { createClient } from 'redis'
const redisUrl = `redis://localhost:6379`;
export const redisClient = createClient({
url: redisUrl
})
const connectRedis = async () => {
try {
console.log('? Redis client connected...');
await redisClient.connect()
redisClient.set(
'trpc',
'welcome to trpc'
)
} catch (e: any) {
await redisClient.disconnect()
console.log(e.message)
process.exit(1)
}
}
connectRedis()
redisClient.on('error', (err) => console.error(err))
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined
}
export const prisma = global.prisma || new PrismaClient()
if (process.env.NODE_ENV == 'development') global.prisma = prisma
export const connectDB = async () => {
try {
await prisma.$connect()
console.log('? Database connected successfully');
} catch (e: any) {
console.error(e.message)
await prisma.$disconnect()
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
> npm install @trpc/server@next @trpc/next@next superjson redis
@trpc/next : 這是 trpc server 管理 procedures 跟 router 的套件。@trpc/next : 讓 trpc server 可以用在 nextjs 的 api handler。superjson : 序列化與反序列化的套件,方便解析 Date 或是 new Set 等等的資料。redis : redis server。
這邊我們簡單寫一個 createContext function 日後可以用到 createNextApiHandler 中, 並使用 inferAsyncReturnType 去自動推論 Context type 是什麼。
//src/server/createContext.ts
import { inferAsyncReturnType } from "@trpc/server";
import { NextApiRequest, NextApiResponse } from "next";
export function createContext({
req,
res,
}: {
req: NextApiRequest;
res: NextApiResponse;
}) {
return { req, res };
}
export type Context = inferAsyncReturnType<typeof createContext>;
這邊我們用 superjson 幫我們做 transformer ,好處就是 transformer 會自動序列化 Date 甚至是 new Set 的資料,這樣就不需要額外處理了。
// src/server/createRouter.ts
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { Context } from "./createContext";
export const t = initTRPC.context<Context>().create({
transformer: superjson,
});
export const publicProcedure = t.procedure
export const router = t.router
這邊簡單測試 redisClient 功能~
import { t } from "../createRouter";
import redisClient from "../utils/connectRedis";
export const appRouter = t.router({
getHello: t.procedure.query(async ({ ctx }) => {
const message = await redisClient.get("trpc");
return { message };
}),
});
export type AppRouter = typeof appRouter;
這邊別放忘記把 createContext 放進去喔~
import * as trpcNext from "@trpc/server/adapters/next";
import { appRouter } from "~/server/routers/app.routes";
import { createContext } from "~/server/createContext";
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
});
測試一下 api ~

> npm install @trpc/react-query@next @trpc/client@next @tanstack/react-query @tanstack/react-query-devtools
這邊特定設定 staleTime 5 秒,如此 5 秒內的 request 永遠都是 refresh的。
import { AppRouter } from '../server/routers/app.routes';
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import SuperJSON from 'superjson';
function getBaseUrl() {
if (typeof window !== 'undefined')
// browser should use relative path
return '';
if (process.env.VERCEL_URL)
// reference for vercel.com
return `https://${process.env.VERCEL_URL}`;
if (process.env.RENDER_INTERNAL_HOSTNAME)
// reference for render.com
return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
// assume localhost
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const api = createTRPCNext<AppRouter>({
config(opts) {
return {
transformer: SuperJSON,
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
// You can pass any HTTP headers you wish here
async headers() {
return {
};
},
}),
],
queryClientConfig: {
defaultOptions: {
queries: {
staleTime: 5 * 1000
}
}
}
};
},
ssr: false,
});
之後記得加 ReactQueryDevtools 到 _app.tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppType } from 'next/app';
import { api } from '~/utils/trpc';
const MyApp: AppType = ({ Component, pageProps }) => {
return (
<>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</>
);
};
export default api.withTRPC(MyApp);
https://github.com/Danny101201/refetch-token/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y