※本記事には広告(楽天アフィリエイト)リンクが含まれる場合があります。

「APIを作るとき、RESTとGraphQLとtRPC、どれを選べばいいの?」——個人開発でバックエンドを設計するとき、2026年の今は選択肢が多くて迷うところです。特にフロントエンドとバックエンドの両方を自分で書く個人開発者にとって、APIの形は開発速度と保守性を大きく左右します。

GraphQLは2015年にFacebook(現Meta)が公開して以来、GitHub・Shopify・Contentfulなど巨大APIの標準として実績を積み、2026年現在も「1リクエストで必要なデータだけを、ネスト構造のまま取得できる」APIとして確固たる地位を築いています。スキーマ(型定義)がAPI仕様書そのものになるため、フロントとバックの連携がスムーズになり、型安全な開発も可能です。

本記事では、GraphQLの基本概念(スキーマ・クエリ・ミューテーション・リゾルバ)から始めて、REST・tRPCとの比較、GraphQL Yoga + Honoによるサーバー実装、Apollo Client / urqlによるクライアント実装、graphql-codegenによる型安全開発、N+1問題とDataLoader、Subscriptionによるリアルタイム、そして本番運用で必須の認証・深さ制限・レート制限まで、個人開発者が「今すぐ使える」コード付きで完全解説します。

広告・楽天アフィリエイト

USBハブ・周辺機器

ドッキング・ケーブル類をまとめて比較。

楽天ポイント還元

周辺機器を楽天で見る →

1. GraphQLとは?REST・tRPCとの違いを整理する

GraphQLは「APIのためのクエリ言語」です。クライアントが欲しいフィールドを指定してリクエストを送り、サーバーはその指定に応じたデータだけを返します。RESTのように「エンドポイントごとに固定のレスポンス」ではなく、1つのエンドポイント(通常は /graphqlで全ての操作を行います。

項目 REST GraphQL tRPC
エンドポイント リソースごとに複数(GET /users, POST /posts…) 原則1つ(POST /graphql) 関数呼び出し(URLは自動生成)
データ取得 エンドポイント固定の形 クライアントがフィールド指定(過剰取得・不足取得が防げる) TypeScript関数を直接呼ぶ
型の共有 OpenAPI等で別途定義 スキーマ(SDL)が唯一の真実 + コード生成で型安全 TypeScriptの型がそのまま共有(コード生成不要)
学習コスト 低い 中程度(スキーマ・リゾルバ・キャッシュ) 低い(TypeScriptさえ分かればOK)
エコシステム 圧倒的 Apollo・GraphQL Yoga・urql・Hasura等が充実 TypeScriptのみ(他言語不可)
リアルタイム WebSocket/SSEを別途 Subscription(標準仕様) 別途WebSocket等が必要
相性の良い用途 シンプルなCRUD・公開API ネストの深いデータ・複数クライアント・大規模API フロントとバックが両方TypeScriptの個人開発

2026年の実務での目安は次の通りです。

  • フロントもバックも自分が書くTypeScriptだけtRPCが最速。型定義の重複が一切ない(tRPC完全ガイド参照)
  • モバイル・Web・サードパーティなど複数クライアントを持つ・公開APIにするGraphQL。言語を問わない標準仕様で、スキーマが自動でAPIドキュメントになる
  • シンプルで公開範囲が限定的 → RESTで十分。API設計ベストプラクティスを参考に

「GraphQLは複雑そう」という印象を持つ人も多いですが、実態は「型定義(スキーマ)+データを返す関数(リゾルバ)」の2つを書くだけです。以下、基本から順に実装していきます。

2. スキーマ定義(SDL)の基本を学ぶ

GraphQLではまずSDL(Schema Definition Language)で「どんなデータがあり、どんな操作ができるか」を定義します。これがAPIの仕様書・契約書になります。

2.1 型(Type)とクエリ・ミューテーション

# schema.graphql — ブログAPIの例
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  createdAt: String!
}

# データ取得の入り口(必須)
type Query {
  user(id: ID!): User
  post(id: ID!): Post
  posts(published: Boolean): [Post!]!
}

# データ変更の入り口
type Mutation {
  createPost(input: CreatePostInput!): Post!
  publishPost(id: ID!): Post!
}

input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
}

!は「null不可」を意味します。[Post!]!は「配列自体もnull不可・中身もnull不可」という意味で、GraphQLでは「可能な限り厳しく型を定義する」のがベストプラクティスです。型を厳しくすると、クライアント側で null チェックの山を書かずに済みます。

2.2 リゾルバ:スキーマとデータを繋ぐ関数

スキーマだけではデータは返りません。リゾルバ(resolver)という「フィールドごとのデータ取得関数」を実装します。

// resolvers.ts(イメージ)
export const resolvers = {
  Query: {
    user: (_, { id }, ctx) => ctx.db.user.findUnique({ where: { id } }),
    posts: (_, { published }, ctx) => ctx.db.post.findMany({ where: { published } }),
  },
  Mutation: {
    createPost: (_, { input }, ctx) => ctx.db.post.create({ data: input }),
  },
  // Post.author は「Postの親からUserを引く」リゾルバ
  Post: {
    author: (post, _, ctx) => ctx.db.user.findUnique({ where: { id: post.authorId } }),
  },
}

ポイントは、ネストされたフィールド(Post.author)にも個別のリゾルバを書けることです。クライアントが author を要求したときだけ呼ばれるため、無駄なJOINを避けられます。ただし、この自由度ゆえに後述のN+1問題が起きるので、注意が必要です(第6章で対策します)。

3. サーバー実装:GraphQL Yoga + Hono(Cloudflare Workers対応)

2026年の個人開発でおすすめのサーバー実装は、GraphQL Yoga(The Guild製)です。Apollo Serverより軽量で、Cloudflare Workers・Bun・Node.jsのどこでも動き、File Upload・SSE/WebSocket Subscription・Persisted Operationsなど現代的な機能が最初から揃っています。

3.1 Hono + GraphQL Yoga で最小構成

Hono(超軽量Webフレームワーク)と組み合わせると、Cloudflare Workers上でGraphQL APIを数分で立てられます。

// src/index.ts — Hono + GraphQL Yoga on Cloudflare Workers
import { Hono } from 'hono'
import { createYoga } from 'graphql-yoga'
import { createSchema } from 'graphql-yoga'

const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      hello: String!
      users: [User!]!
    }
    type User {
      id: ID!
      name: String!
    }
  `,
  resolvers: {
    Query: {
      hello: () => 'Hello GraphQL!',
      users: () => [{ id: '1', name: 'Taro' }, { id: '2', name: 'Hanako' }],
    },
  },
})

const yoga = createYoga({ schema, graphqlEndpoint: '/graphql' })
const app = new Hono()
app.on(['GET', 'POST'], '/graphql', (c) => yoga.fetch(c.req.raw, c.env))

export default app

デプロイは wrangler deploy だけで完了します。Cloudflare WorkersでマイクロSaaSを作るで紹介した構成にそのまま組み込めるので、バックエンドが一気に「型定義+リゾルバ」の世界になります。

3.2 データベースとの接続

実データはDrizzle ORMやPrismaで取得するのが定番です。Drizzle ORM完全ガイドで紹介した通り、Cloudflare D1とDrizzleの組み合わせはWorkers上でそのまま動作します。

// Drizzle + D1 をリゾルバから使う
import { drizzle } from 'drizzle-orm/d1'
import { users, posts } from './schema'

export const resolvers = {
  Query: {
    user: async (_, { id }, ctx) => {
      const db = drizzle(ctx.env.DB)   // D1 binding
      const rows = await db.select().from(users).where(eq(users.id, id))
      return rows[0] ?? null
    },
  },
}

データベース選びに迷ったら個人開発者向けデータベース完全比較を参照してください。SQLite(D1)で十分な規模が大半です。

4. クライアント実装:Apollo Client / urql

次に、フロントエンドからGraphQL APIを呼びます。Reactの定番はApollo Client(高機能・キャッシュ強力)とurql(軽量・シンプル)の2つです。

4.1 Apollo Client の基本

// Apollo Client セットアップ(Next.js App Router の場合)
import { ApolloClient, InMemoryCache, gql, useQuery } from '@apollo/client'

const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache(),
})

const GET_POSTS = gql`
  query GetPosts {
    posts(published: true) {
      id
      title
      author { name }
    }
  }
`

function PostList() {
  const { data, loading, error } = useQuery(GET_POSTS)
  if (loading) return <p>読み込み中…</p>
  if (error) return <p>エラー: {error.message}</p>
  return (
    <ul>
      {data.posts.map((post) => (
        <li key={post.id}>{post.title}({post.author.name})</li>
      ))}
    </ul>
  )
}

Apollo Clientの強みは正規化キャッシュです。同じ User(id: 1) を別クエリで取得しても、キャッシュ上の同一オブジェクトとして扱われるため、無駄な再取得が減ります。キャッシュ戦略の考え方はReactデータフェッチ完全ガイド(TanStack Query編)とも共通する部分が多いです。

4.2 ミューテーション(データ変更)

const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
    }
  }
`

function NewPostForm() {
  const [createPost, { loading }] = useMutation(CREATE_POST)
  return (
    <form onSubmit={async (e) => {
      e.preventDefault()
      await createPost({
        variables: {
          input: { title: '新記事', content: '本文', authorId: '1' },
        },
      })
    }}>
      <button type="submit" disabled={loading}>投稿する</button>
    </form>
  )
}

ミューテーション後のキャッシュ更新は、refetchQueries(再取得)か update 関数(キャッシュ直接書き換え)で行います。まずは refetchQueries: [{ query: GET_POSTS }] から始めるのが分かりやすいです。

5. コード生成(graphql-codegen)で型安全に開発する

GraphQL最大の魅力の1つが、スキーマから型を自動生成できることです。フロントエンドの型定義を手書きする必要がなくなり、「スキーマを変えたら型エラーで一括検出」という開発体験が得られます。

# codegen.ts — graphql-codegen 設定
import type { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  schema: 'https://api.example.com/graphql',   // スキーマの取得元
  documents: ['src/**/*.tsx'],                 // クエリが書かれたファイル
  generates: {
    './src/gql/': {
      preset: 'client',                        // クライアントpreset(推奨)
      plugins: [],
    },
  },
}
export default config
# 実行
npx graphql-codegen

生成された型は次のように使います。クエリ文字列から自動で型が推論されるので、data.posts[0].author.name が存在しないフィールドならコンパイルエラーになります。

import { graphql } from '@/gql'
import { useQuery } from '@apollo/client'

const GetPosts = graphql(/* GraphQL */ `
  query GetPosts {
    posts(published: true) {
      id
      title
      author { name }
    }
  }
`)

function PostList() {
  // data が完全に型付き!
  const { data } = useQuery(GetPosts)
  return <ul>{data?.posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}

使ってみた感想: コード生成を入れると、API変更に対する恐怖が消えます。スキーマ側でフィールドをリネームすれば、使っている全箇所が型エラーとして浮き上がってくるからです。「フロントとバックで型定義を二重管理する」問題をGraphQLは仕組みで解決してくれます。

広告・楽天アフィリエイト

デスクチェア

長時間コーディング向けの椅子を探す。

楽天ポイント還元

チェアを楽天で見る →

6. N+1問題とDataLoaderでパフォーマンスを改善する

GraphQLの自由度が招く代表的な罠がN+1問題です。一覧クエリ posts { author { name } } を実行すると、posts のリゾルバ1回+各Postの author リゾルバN回、つまり合計 N+1回のDBアクセスが発生します。

// N+1問題の例:postsが100件なら101回DBアクセス
Query.posts   → SELECT * FROM posts            (1回)
Post.author   → SELECT * FROM users WHERE id=1 (100回)
Post.author   → SELECT * FROM users WHERE id=2
Post.author   → SELECT * FROM users WHERE id=3
...

これを解決するのがDataLoaderです。リクエスト内で同じキーの取得をバッチ化+キャッシュしてくれます。

// DataLoader で author 取得をバッチ化
import DataLoader from 'dataloader'

// 複数のIDをまとめて1回のクエリで取得
const userLoader = new DataLoader(async (ids: readonly string[]) => {
  const rows = await db.select().from(users).where(inArray(users.id, [...ids]))
  const byId = new Map(rows.map((r) => [r.id, r]))
  return ids.map((id) => byId.get(id) ?? null)
})

export const resolvers = {
  Post: {
    author: (post, _, ctx) => userLoader.load(post.authorId),
  },
}

DataLoaderを使うと、100件の投稿でも author 取得は1回のDBクエリに集約されます。load() の呼び出しは同一ティック内でまとめられ、IN (…) クエリ1本になります。GraphQLサーバーを本番運用するなら、DataLoaderは「入れて当然」のレベルなので必ず押さえましょう。

その他のパフォーマンス対策としては、Persisted Operations(クエリをサーバー側に事前登録して、クライアントはIDだけ送る)やHTTPキャッシュ(GETクエリに Cache-Control を付ける)があります。キャッシュ設計の考え方はCore Web Vitals完全ガイドUpstash Redisガイドも参考になります。

7. Subscriptionでリアルタイム更新を実装する

GraphQLのSubscriptionを使うと、サーバーからクライアントへリアルタイムにデータをプッシュできます(チャット・在庫通知・タスクボードのライブ更新など)。WebSocketの上にGraphQLの操作を流す仕組みです。リアルタイム通信全般の基礎はWebSocket・SSEリアルタイム通信完全ガイドで解説しています。

// スキーマに Subscription を追加
type Subscription {
  postPublished: Post!
}
// サーバー(GraphQL Yoga は pub/sub を内蔵)
import { createPubSub } from 'graphql-yoga'

const pubSub = createPubSub()

export const resolvers = {
  Subscription: {
    postPublished: {
      subscribe: () => pubSub.subscribe('POST_PUBLISHED'),
      resolve: (payload) => payload,
    },
  },
  Mutation: {
    publishPost: async (_, { id }, ctx) => {
      const post = await ctx.db.post.update({ where: { id }, data: { published: true } })
      await pubSub.publish('POST_PUBLISHED', post)   // 購読者へ通知
      return post
    },
  },
}
// クライアント(Apollo Client)
import { useSubscription, gql } from '@apollo/client'

const POST_PUBLISHED = gql`
  subscription PostPublished {
    postPublished { id title }
  }
`

function LiveFeed() {
  const { data } = useSubscription(POST_PUBLISHED)
  return <p>新しい投稿: {data?.postPublished?.title}</p>
}

Cloudflare WorkersでSubscriptionを使う場合、GraphQL YogaはSSE(Server-Sent Events)ベースのSubscriptionにも対応しており、WebSocketを張り続けるより安価に運用できます。まずはSSE、必要になってからWebSocketへ、という段階が個人開発には合っています。

8. セキュリティ:認証・深さ制限・レート制限

GraphQLは自由度が高い分、攻撃面も広がりやすいので、公開APIにする前に次の3点は必ず実装しましょう。

8.1 認証・認可

認証は通常のWeb APIと同じく、AuthorizationヘッダーやCookieでトークンを検証し、context にユーザー情報を載せます。リゾルバ側で「このユーザーはこのデータを見ていいか」を判定します。認証基盤の選定は認証サービス完全比較を参照してください。

// Yoga の context で認証情報を解決
const yoga = createYoga({
  schema,
  context: async ({ request }) => {
    const token = request.headers.get('authorization')?.replace('Bearer ', '')
    const user = token ? await verifyToken(token) : null
    return { user }
  },
})

// リゾルバ内で認可チェック
export const resolvers = {
  Mutation: {
    publishPost: async (_, { id }, ctx) => {
      if (!ctx.user) throw new GraphQLError('認証が必要です', { extensions: { code: 'UNAUTHENTICATED' } })
      // 自分の投稿かチェック…
    },
  },
}

8.2 深さ制限(Depth Limit)

GraphQLはクエリを自由にネストできるため、再帰的なクエリでサーバーを叩き潰す攻撃(例: { user { posts { author { posts { … } } } } })が可能です。graphql-depth-limit などで深さを制限しましょう。

import depthLimit from 'graphql-depth-limit'

const yoga = createYoga({
  schema,
  // 深さ10まで許可(通常のアプリなら十分)
  plugins: [useDepthLimit({ maxDepth: 10 })],
})

8.3 レート制限とクエリ複雑度

エンドポイントが1つなので、IP・ユーザー単位のレート制限を掛けやすいのがGraphQLの利点でもあります。さらに、クエリ複雑度(Complexity)を計算して、重いクエリを拒否する方法もあります(例: 各フィールドに重みを付け、合計が閾値を超えたらエラー)。API全体のレート制限設計はAPI設計ベストプラクティスで詳しく解説しています。

9. まとめ:GraphQLを選ぶべき人・避けるべき人

最後に、GraphQL導入の判断基準を整理します。

  1. 複数クライアント(Web・モバイル・外部)を持つ・公開APIを作る → GraphQLが本領発揮。スキーマがそのままドキュメントになり、各クライアントが必要なデータだけを取得できる
  2. 型安全な開発体験が欲しい → graphql-codegenでスキーマから型を自動生成。フロントとバックの型の二重管理から解放される
  3. フロントもバックも自分だけのTypeScripttRPCの方がコード生成なしで速い。GraphQLはオーバーキルになりがち
  4. 導入時の注意 → N+1問題(DataLoader)、深さ制限・レート制限(セキュリティ)、キャッシュ設計(Apolloの正規化キャッシュ)の3つを最初から計画に含める

サーバーはGraphQL Yoga + Hono(Cloudflare Workersで動作)、クライアントはApollo Client、型はgraphql-codegen——この4点セットが2026年の個人開発におけるGraphQLの定番構成です。REST・tRPC・GraphQLは「どれが正解」ではなく「作るものに合わせて選ぶ」もの。本記事を判断材料に、自分に合ったAPI設計を選んでください。

関連記事として、tRPC完全ガイド(TypeScript特化の代替)、API設計ベストプラクティスHono + Cloudflare WorkersガイドReactデータフェッチ完全ガイドもあわせてどうぞ。型安全なAPI開発を始めましょう。