API 開発
oRPC で型安全な API を構築
概要
EasyStarter は oRPC を使用してエンドツーエンドの型安全な API 開発を行い、tRPC と OpenAPI の良いところを組み合わせています。
アーキテクチャ
apps/server/src/
├── routers/ # API ルーター
│ ├── index.ts # メインルーター
│ └── user.ts # ユーザー関連プロシージャ
├── handlers/ # ビジネスロジック
└── middlewares/ # 認証、ログなどルーターの作成
プロシージャの定義
// apps/server/src/routers/post.ts
import { publicProcedure, protectedProcedure, router } from "../lib/orpc";
import { z } from "zod";
export const postRouter = router({
// パブリックプロシージャ
list: publicProcedure
.input(
z.object({
page: z.number().default(1),
limit: z.number().default(10),
}),
)
.query(async ({ input, ctx }) => {
return ctx.db.select().from(posts).limit(input.limit);
}),
// 保護されたプロシージャ(認証が必要)
create: protectedProcedure
.input(
z.object({
title: z.string().min(1),
content: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
return ctx.db.insert(posts).values({
...input,
authorId: ctx.user.id,
});
}),
});ルーターの登録
// apps/server/src/routers/index.ts
import { postRouter } from "./post";
export const appRouter = router({
post: postRouter,
// ... 他のルーター
});
export type AppRouter = typeof appRouter;フロントエンドでの使用
クライアントの設定
// apps/web/src/lib/api.ts
import { createORPCClient } from "@orpc/client";
import type { AppRouter } from "@server/routers";
export const api = createORPCClient<AppRouter>({
baseURL: import.meta.env.VITE_SERVER_URL,
});データのクエリ
// React Query を使用
const { data, isLoading } = api.post.list.useQuery({
page: 1,
limit: 10,
});データの変更
const createPost = api.post.create.useMutation({
onSuccess: () => {
queryClient.invalidateQueries(["post", "list"]);
},
});
await createPost.mutateAsync({
title: "Hello World",
content: "私の最初の投稿",
});ミドルウェア
認証ミドルウェア
const authMiddleware = middleware(async ({ ctx, next }) => {
const session = await getSession(ctx.request);
if (!session) {
throw new ORPCError("UNAUTHORIZED");
}
return next({ ctx: { ...ctx, user: session.user } });
});
export const protectedProcedure = publicProcedure.use(authMiddleware);エラーハンドリング
import { ORPCError } from "@orpc/server";
throw new ORPCError("NOT_FOUND", { message: "投稿が見つかりません" });
throw new ORPCError("FORBIDDEN", { message: "アクセスが拒否されました" });
throw new ORPCError("BAD_REQUEST", { message: "入力が無効です" });