スキーマ駆動開発の実践:React × Express × GraphQLで効率的なAPI設計を実現
はじめに
近年、フロントエンドとバックエンドの開発を効率化するアプローチとして スキーマ駆動開発(Schema-Driven Development, SDD) が注目されています。
GraphQL を活用した SDD では、まず GraphQL のスキーマを定義し、それを基にバックエンドとフロントエンドを実装 していきます。
これにより、API の仕様が明確になり、フロントエンド・バックエンドの開発を並行して進めることが可能になります。
本記事では、React(Vite)+ Express(Apollo Server)を使用して、スキーマ駆動開発の実践方法を解説 します。
また、GraphQL のスキーマから TypeScript の型を自動生成することで、型安全な開発を実現 します。
この記事を読むことで、以下のことが学べます:
- スキーマ駆動開発のメリット
- React(Vite)+ Express(Apollo Server)を活用した GraphQL API の構築
- GraphQL スキーマを基に TypeScript の型を自動生成
- フロントエンドとバックエンドの連携方法
スキーマ駆動開発とは
スキーマ駆動開発(SDD: Schema-Driven Development)とは、API の仕様を GraphQL のスキーマ定義(SDL: Schema Definition Language)を基盤に設計・開発を進める手法 です。
従来の REST API では、API 仕様書を別途作成し、フロントエンドとバックエンド間で共有する必要がありました。しかし、GraphQL ではスキーマがそのまま API の仕様となるため、開発の進行がスムーズになります。
メリット
- 仕様の明確化
スキーマがそのまま API 仕様となるため、フロントエンド / バックエンド間で仕様の認識相違が起きづらくなります。 - 自動ドキュメント化
GraphQL Playground や Apollo Studio などを使えば、スキーマ定義から自動的にドキュメントを生成でき、API の利用者が理解しやすくなります。 - 変更点の検知が容易
スキーマに変更がある場合、実装との整合性チェックも自動で発生しやすくなるため、変更の影響範囲が明確になります。
このステップを踏むことで、チーム全体の作業効率も上がり、スムーズなコミュニケーションとプロジェクト運営を実現できます。
今回のゴール
下記の流れで実装を進めていきます。
- GraphQLスキーマ定義を作成
- サーバーでスキーマ定義を読み込んでGraphQLサーバーを起動
- GraphQLスキーマ定義、GraphQLクエリからTypeScriptの型定義を作成
- フロントエンドからGraphQLでサーバーのデータを取得
今回は投稿データをサンプルとして扱い、
最終的には投稿データのタイトルおよび著者の情報をブラウザで表示させます。
セットアップ
まずは全体のプロジェクトのセットアップから始めます。
mkdir Documents/workspace/schema-driven-development-react-express-graphql
cd Documents/workspace/schema-driven-development-react-express-graphql
npm init -y
Turborepo
を使えるようインストールします。
npm install turbo -D
Turborepo(ターボレポ)は、モノレポ(monorepo)構成を管理するためのツールで、主にJavaScriptやTypeScriptのプロジェクトに利用されます。モノレポとは、複数のプロジェクトやパッケージを一つのリポジトリ内で管理するアプローチです。Turborepoは、このようなモノレポを効率的に扱うためのツールです。
次にTurborepo
の設定を行います。
タスクという形でビルドや開発モードでの起動などの設定ができます。
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
次にルート直下のpackage.jsonの設定をします。
{
"name": "react-express-graphql-turborepo",
+ "workspaces": [
"apps/*",
"packages/*"
],
"version": "1.0.0",
"description": "",
+ "packageManager": "npm@10.9.2",
- "main": "index.js",
"scripts": {
+ "dev": "turbo dev",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"turbo": "^2.4.0"
}
}
workspaces
: モノレポ(monorepo)を構築するための設定です。モノレポとは、複数のパッケージやアプリケーションを単一のリポジトリ内で管理するアプローチです。
workspacesを使用すると、異なるパッケージ(アプリケーションやライブラリ)を同じリポジトリ内で効率的に管理でき、依存関係の共有やインストールの高速化、開発の効率化ができます。
フロントエンド(React + Vite)の作成
まずはフロントエンドから設定してきます。
mkdir apps
cd apps
npm create vite@latest frontend
viteでフロントエンドを構築する際に幾つか質問が出ますが、下記で設定します。
Select a framework: React
Select a variant: TypeScript + SWC
次にスコープ付きパッケージの設定をします。
{
- "name": "frontend",
+ "name": "@react-express-graphql-turborepo/frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0"
}
}
ViteとReactの必要なファイルをダウンロードできたのでパッケージをインストールします。
cd apps/frontend
npm install
インストールが終わったら念のため起動し動作確認をしておきます。
npm run dev
http://localhost:5173/
にアクセスできればフロントエンドの設定完了です。
バックエンド(Experss.js)の作成
次にバックエンドとしてExpressの設定を行います。
まずはパッケージのインストールを行います。
mkdir apps/backend && cd apps/backend
npm init -y
npm i express cors dotenv
npm i -D typescript ts-node @types/node @types/express @types/cors
その後、スコープ付きパッケージの設定およびtsconfig.json
の設定を行います。
{
- "name": "backend",
+ "name": "@react-express-graphql-turborepo/backend",
"version": "1.0.0",
{
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"strict": true,
"module": "CommonJS",
"target": "ES6",
"esModuleInterop": true
}
}
Express
を動かせる環境ができましたので、起動するファイルを作成します。
import express from "express";
import cors from "cors";
const app = express();
const PORT = process.env.PORT || 4000;
app.use(cors());
app.use(express.json());
app.get("/", (req, res) => {
res.send("Hello from Express!");
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
/
でGETリクエストを送信すると、Hello from Express!
という文字列が返却されます
ファイルが作成できましたら、package.json
で起動する処理を定義します。
- "main": "index.js",
+ "main": "index.ts",
"scripts": {
+ "dev": "ts-node src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
念のためExpress
が起動できるか確認します。
npm run dev
Server is running on http://localhost:4000
起動できましたら、curl
でレスポンスを確認します。
curl http://localhost:4000
Hello from Express!
以上でExperss
のセットアップは終了となります。
フロントエンドとバックエンドの連携
fetch
モジュールを使ってExpressにアクセスしGETメソッドのレスポンスであるHello from Express!
をブラウザで表示させます。
import { useEffect, useState } from "react";
function App() {
const [message, setMessage] = useState("");
useEffect(() => {
fetch("http://localhost:4000")
.then((res) => res.text())
.then((data) => setMessage(data));
}, []);
return (
<div>
<h1>React + Express</h1>
<p>API Response: {message}</p>
</div>
);
}
export default App;
初期設定のCSSが残って影響を与えるので一旦削除します。
rm apps/frontend/src/index.css
rm apps/frontend/src/App.css
またcssを読み込んでいるファイルも修正しておきます。
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
- import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
一通り設定ができましたのでReactとExpressをTurborepoから起動します。
ルート直下で下記コマンドを実行します。
npm run dev
turborepoを使ってフロントエンド、バックエンドのリポジトリを管理し、双方を連携できました。
ここから今回のブログの本題である「スキーマ駆動開発」を実施していきたいと思います。
スキーマ定義作成
GraphQLの定義をフロントエンドとバックエンドの両方から参照できるようにするために、モノレポの共通ディレクトリに配置していきます。
mkdir apps/packages
mkdir apps/packages/graphql
mkdir apps/packages/graphql/schema
この GraphQL スキーマは、ブログの投稿 (Post
) とその著者 (Author
) を扱う API です。
type Author {
id: Int!
firstName: String!
lastName: String!
}
type Post {
id: Int!
title: String!
author: Author
}
type Query {
posts: [Post] # 単一の投稿を取得する
post(id: ID!): Post # 単一の投稿を取得する
}
-
Author
は投稿 (Post
) を書いた著者id: Int!
→ 著者のユニークなID(!
は必須フィールドを意味する)firstName: String!
→ 著者の名前(必須)lastName: String!
→ 著者の名字(必須)
-
Post
はブログの投稿データid: Int!
→ 投稿のユニークなID(必須)title: String!
→ 投稿のタイトル(必須)author: Author
→ 投稿の著者(Author 型)
-
author: Author
を定義することで、投稿 (Post
) から著者 (Author
) を取得できます -
クライアントが呼び出せるエンドポイント
posts: [Post]
→ すべての投稿を取得するpost(id: ID!): Post
→ ID を指定して特定の投稿を取得するid: ID!
は GraphQL のID
型(通常String
やInt
を格納)
バックエンド実装
ExpressでGraphQLサーバーを起動するために幾つかライブラリをインストールします。
npm i @apollo/server graphql graphql-tag graphql-tools/load-files
npm i -D @types/express-serve-static-core
パッケージ名 | 説明 |
---|---|
@apollo/server | Apollo Server(GraphQL API を提供するサーバーライブラリ) |
graphql | GraphQL のコアライブラリ(スキーマのパース、バリデーションなど) |
graphql-tag | GraphQL のクエリ (gql テンプレート) をパースするユーティリティ |
graphql-tools/load-files | GraphQL のスキーマやリゾルバーをファイルから読み込むライブラリ |
@types/express-serve-static-core | express の型定義(express の内部機能 serve-static などを含む) |
次にリゾルバの定義をします。
// ダミーデータ
const posts = [
{ id: 1, title: "GraphQL Introduction", authorId: 1 },
{ id: 2, title: "Advanced GraphQL", authorId: 1 },
{ id: 3, title: "GraphQL with Apollo", authorId: 2 },
];
export const resolvers = {
Query: {
// すべての投稿を取得
posts: () => posts,
// IDで特定の投稿を取得
post: (_parent: any, args: { id: number }) =>
posts.find((post) => post.id === args.id),
},
};
-
GraphQL の
Query.posts
に対応するリゾルバー- 全ての投稿 (
posts
配列) を返す - データベース不要で簡単に動作
- 全ての投稿 (
-
GraphQL の
Query.post(id: ID!)
に対応するリゾルバーargs.id
の値に一致する投稿 (post.id
) を 検索- 該当する
post
があれば返し、なければundefined
を返す - データがないと
null
になる可能性があるので、クライアント側で処理が必要
次にGraphQL スキーマ (.graphql
ファイル) を読み込んで Apollo Serverにスキーマ定義を渡せるための設定を行います。
import { readFileSync } from "fs";
import { gql } from "graphql-tag";
import path from "path";
const schemaPath = path.resolve(process.cwd(), "../packages/graphql/schema/post.graphql");
const typeDefs = gql(readFileSync(schemaPath, "utf-8"));
export { typeDefs };
- GraphQL のクエリやスキーマを
gql
(GraphQL タグ)としてパースするgraphql-tag
は、文字列として読み込んだ GraphQL スキーマをgql
タグで解析するためのライブラリgql
を使うことで、Apollo Server に正しくスキーマを渡せる
import express from "express";
import cors from "cors";
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { resolvers } from './graphql/resolvers';
import { typeDefs } from "./graphql/type";
const server = new ApolloServer({ typeDefs, resolvers });
const app = express();
const PORT = process.env.PORT || 4000;
const startServer = async () => {
await server.start();
app.use(
'/graphql',
cors(),
express.json(),
expressMiddleware(server)
);
app.listen(PORT, () => {
console.log('GraphQL server is running at http://localhost:4000/graphql');
});
};
startServer();
-
ライブラリ
express
→ HTTP サーバーを作成するためのフレームワークcors
→ CORS(クロスオリジンリクエスト)を許可するためのミドルウェア@apollo/server
→ Apollo Server(GraphQL API を提供するライブラリ)expressMiddleware
→ Express で Apollo Server をミドルウェアとして組み込むための関数resolvers
→ GraphQL のリゾルバー(クエリを処理する関数群)typeDefs
→ GraphQL のスキーマ(データの構造を定義)
-
GraphQL サーバー (Apollo Server) を作成
- typeDefs(スキーマ)と resolvers(データ取得の処理)を Apollo Server に渡す
- この server が GraphQL API のエンドポイントを提供
-
GraphQL サーバー (server) を Express に組み込んで起動
server.start()
→ Apollo Server を起動app.use('/graphql', ...)
→/graphql
エンドポイントを作成cors()
→ CORS を許可(ブラウザからのリクエストを制限しない)express.json()
→ JSON 形式のリクエストをパースexpressMiddleware(server)
→ Apollo Server を Express のミドルウェアとして登録
app.listen(PORT, () => { ... })
→ 指定したポート (4000) でサーバーを起動
設定が終わりましたらExpressサーバーを起動した後にcurl
でレスポンスが返ってくるか確認します。
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ posts { id title } }"}' | jq
レスポンスとしてリゾルバで設定したダミーデータが返ってきたらサーバー側の設定は完了です。
{
"data": {
"posts": [
{
"id": 1,
"title": "GraphQL Introduction"
},
{
"id": 2,
"title": "Advanced GraphQL"
},
{
"id": 3,
"title": "GraphQL with Apollo"
}
]
}
}
おまけ:タイトルによるフィルター
クエリ実行時にtitleでフィルターをかける設定を入れてみます。
export const resolvers = {
Query: {
- // すべての投稿を取得
+ // タイトルでフィルタリング可能な全投稿取得
- posts: () => posts,
+ posts: (_parent: any, args: { findTitle?: string }) => {
+ let filteredPosts = posts;
+ if (args.findTitle) {
+ filteredPosts = filteredPosts.filter((post) =>
+ post.title.toLowerCase().includes(args.findTitle!.toLowerCase())
+ );
+ }
return filteredPosts;
},
// IDで特定の投稿を取得
post: (_parent: any, args: { id: number }) =>
posts.find((post) => post.id === args.id),
},
};
findTitle
があった場合はタイトルと部分一致したものを返却します。findTitle
がない場合は全件を返却します。
クエリ定義でfindTitle
を引数に設定します。
type Query {
- posts: [Post]
+ posts(findTitle: String): [Post] # タイトルでフィルタリング可能に
post(id: ID!): Post # 単一の投稿を取得する
}
curl
$ curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ posts(findTitle: \"Apollo\") { id title } }"}' | jq
{
"data": {
"posts": [
{
"id": 3,
"title": "GraphQL with Apollo"
}
]
}
}
Apolloという文字が入ったタイトルのみレスポンスにセットされています。
スキーマを基に TypeScript 型を自動生成
次はフロントエンドでTypeScript型を使用するためにGraphQL定義から生成する対応を行います。
GraphQL Code Generatorのインストール
npm i -D @graphql-codegen/cli
GraphQL Code Generator (@graphql-codegen/cli
) の設定ファイル を作成し、
GraphQL のスキーマとクエリから 型付きの TypeScript コードを自動生成するための設定を行います。
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: '../packages/graphql/schema/**/*.graphql',
generates: {
"generated/graphql.ts": {
plugins: [
'typescript',
'typescript-operations',
],
},
}
};
export default config;
-
schema
: GraphQL のスキーマファイルのパスを指定../packages/graphql/schema/**/*.graphql
packages/graphql/schema/
配下の すべての.graphql
ファイル を対象**/*.graphql
は 再帰的にすべての GraphQL スキーマを探す
- スキーマには
type Query
,type Mutation
,type Post
などの型定義が含まれる
-
generates
: どのファイルに型を生成するか設定"generated/graphql.ts"
→generated/graphql.ts
に TypeScript 型を生成plugins
→ どのプラグインを使うか指定
-
plugins
: GraphQL のスキーマとクエリから型を生成するプラグイン'typescript'
→ GraphQL のスキーマ (schema.graphql
) から TypeScript 型を生成'typescript-operations'
→ GraphQL クエリ (.graphql
) から TypeScript 型を生成
{
"name": "@react-express-graphql-turborepo/frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
+ "codegen": "graphql-codegen --config scripts/codegen.ts"
},
graphql-codegen
を実行するためのコマンドを設定
npm run codegen
generated/graphql.ts
に TypeScript 型が生成されます。
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
};
export type Author = {
__typename?: 'Author';
firstName: Scalars['String']['output'];
id: Scalars['Int']['output'];
lastName: Scalars['String']['output'];
posts?: Maybe<Array<Maybe<Post>>>;
};
export type AuthorPostsArgs = {
findTitle?: InputMaybe<Scalars['String']['input']>;
};
export type Post = {
__typename?: 'Post';
author?: Maybe<Author>;
id: Scalars['Int']['output'];
title: Scalars['String']['output'];
};
export type Query = {
__typename?: 'Query';
post?: Maybe<Post>;
posts?: Maybe<Array<Maybe<Post>>>;
};
export type QueryPostArgs = {
id: Scalars['ID']['input'];
};
これでGraphQLスキーマ定義からTypeScriptの型定義を生成できました。
複数のGraphQLスキーマ定義を作成しても一括してTypeScriptの型定義を生成できます。
type Recipe {
id: Int!
title: String!
imageUrl: String
}
type Query {
recipe(id: Int!): Recipe!
}
再度、実行
npm run codegen
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
};
export type Author = {
__typename?: 'Author';
firstName: Scalars['String']['output'];
id: Scalars['Int']['output'];
lastName: Scalars['String']['output'];
posts?: Maybe<Array<Maybe<Post>>>;
};
export type AuthorPostsArgs = {
findTitle?: InputMaybe<Scalars['String']['input']>;
};
export type Post = {
__typename?: 'Post';
author?: Maybe<Author>;
id: Scalars['Int']['output'];
title: Scalars['String']['output'];
};
export type Query = {
__typename?: 'Query';
post?: Maybe<Post>;
posts?: Maybe<Array<Maybe<Post>>>;
recipe: Recipe;
};
export type QueryPostArgs = {
id: Scalars['ID']['input'];
};
export type QueryRecipeArgs = {
id: Scalars['Int']['input'];
};
export type Recipe = {
__typename?: 'Recipe';
id: Scalars['Int']['output'];
imageUrl?: Maybe<Scalars['String']['output']>;
title: Scalars['String']['output'];
};
Query
にはposts
とrecipe
が生成されており、まとめて処理されていることがわかります。
フロントエンド実装
まずはGraphQLクエリを作成します。
この GraphQL クエリ GetPosts
は、
すべての投稿 (posts
) を取得し、それぞれの投稿の id・title・著者 (author
) の情報を取得する クエリです。
query GetPosts {
posts {
id
title
author {
firstName
lastName
}
}
}
GraphQL クエリを型生成できるようconfig.tsの設定を追加します。
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: '../packages/graphql/schema/**/*.graphql',
+ documents: '../packages/graphql/operations/**/*.graphql',
generates: {
"generated/graphql.ts": {
plugins: [
'typescript',
'typescript-operations',
],
},
}
};
export default config;
すると下記のGetPostsQuery
が生成されます。
getPostsの戻り値の型になります。
export type GetPostsQuery = {
__typename?: 'Query',
posts?: Array<{
__typename?: 'Post',
id: number,
title: string,
author?: {
__typename?: 'Author',
firstName: string,
lastName: string
} | null
} | null> | null
};
npm i -D @graphql-codegen/typescript-graphql-request
npm i graphql-request
@graphql-codegen/typescript-graphql-request
は、GraphQL Code Generator のプラグインの一つで、GraphQL のスキーマやクエリから graphql-request
に適した TypeScript の型やクライアントコードを自動生成するためのものです。
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: '../packages/graphql/schema/**/*.graphql',
documents: '../packages/graphql/operations/**/*.graphql',
generates: {
"generated/graphql.ts": {
plugins: [
'typescript',
'typescript-operations',
+ 'typescript-graphql-request',
],
},
}
};
export default config;
Code Generatorのconfigで追加し、再度型を生成すると下記が追加されます。
export const GetPostsDocument = gql`
query GetPosts {
posts {
id
title
author {
firstName
lastName
}
}
}
`;
export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string, operationType?: string, variables?: any) => Promise<T>;
const defaultWrapper: SdkFunctionWrapper = (action, _operationName, _operationType, _variables) => action();
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
return {
GetPosts(variables?: GetPostsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<GetPostsQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetPostsQuery>(GetPostsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'GetPosts', 'query', variables);
}
};
}
export type Sdk = ReturnType<typeof getSdk>;
import { GraphQLClient } from "graphql-request";
import { getSdk } from "../generated/graphql";
const client = new GraphQLClient("http://localhost:4000/graphql");
const sdk = getSdk(client);
export async function getPost() {
const response = await sdk.GetPosts();
return response.posts
}
ポイント
graphql-request
は、シンプルな GraphQL クライアントライブラリ で、HTTP リクエストを GraphQL API に送信するために使います。getSdk
はGraphQL Code Generator
によって自動生成された API クライアント です。graphql-code-generator
を使うと、GraphQL のクエリを TypeScript の型付き関数 (getSdk
) に変換できる。getSdk
を使うことで、GraphQL クエリを簡単に実行し、型安全なデータ取得ができます。
import { useEffect, useState } from "react";
import { GetPostsQuery } from "../generated/graphql";
import { getPosts } from "./service";
function App() {
const [posts, setPosts] = useState<GetPostsQuery["posts"] | null>(null);
useEffect(() => {
getPosts()
.then((data) => {
setPosts(data);
})
.catch((error) => {
console.error(error);
});
}, []);
return (
<div>
<h1>React + Express</h1>
{posts === null || posts === undefined ? (
<p>Loading...</p>
) : posts.length === 0 ? (
<p>No posts found.</p>
) : (
<ul>
{posts.map((post) => (
<li key={post?.id}>
<h2>{post?.title}</h2>
<p>Author: {post?.author?.firstName} {post?.author?.lastName}</p>
</li>
))}
</ul>
)}
</div>
);
}
export default App;
GetPostsQuery
がgetPosts
の戻り値の型として使えます。
結果を確認したところ、投稿タイトルは表示されていますが、著者が表示されていません。
リゾルバを再度確認します。
どうやら投稿一覧を返すときに著者の情報をセットするのを忘れていたようです。
export const resolvers = {
Query: {
// タイトルでフィルタリング可能な全投稿取得
posts: (_parent: any, args: { findTitle?: string }) => {
let filteredPosts = posts;
if (args.findTitle) {
filteredPosts = filteredPosts.filter((post) =>
post.title.toLowerCase().includes(args.findTitle!.toLowerCase())
);
}
return filteredPosts;
},
// IDで特定の投稿を取得
post: (_parent: any, args: { id: number }) =>
posts.find((post) => post.id === args.id),
},
};
著者の情報を設定するよう修正します。
なお、こちらの情報もダミーデータで今回は一旦対応します。
// ダミーデータ
+ const authors = [
+ { id: 1, firstName: "John", lastName: "Doe" },
+ { id: 2, firstName: "Jane", lastName: "Smith" },
+ ];
const posts = [
{ id: 1, title: "GraphQL Introduction", authorId: 1 },
{ id: 2, title: "Advanced GraphQL", authorId: 1 },
{ id: 3, title: "GraphQL with Apollo", authorId: 2 },
];
export const resolvers = {
Query: {
// タイトルでフィルタリング可能な全投稿取得 + `author` を追加
posts: (_parent: any, args: { findTitle?: string }) => {
+ let filteredPosts = posts;
if (args.findTitle) {
filteredPosts = filteredPosts.filter((post) =>
post.title.toLowerCase().includes(args.findTitle!.toLowerCase())
);
}
+ // `authorId` を `author` に変換
+ return filteredPosts.map((post) => ({
+ ...post,
+ author: authors.find((author) => author.id === post.authorId) || null,
+ }));
},
// IDで特定の投稿を取得 + `author` を追加
post: (_parent: any, args: { id: number }) => {
const post = posts.find((post) => post.id === args.id);
if (!post) return null;
return {
...post,
author: authors.find((author) => author.id === post.authorId) || null,
};
},
},
};
再度、ブラウザで確認をすると著者の情報も正しくセットされていました。
ディレクトリ構成
最後にディレクトリ構成を確認し全体像を改めて整理しておきます。
tree -I node_modules -I .git -I .turbo --dirsfirst -a
.
├── apps
│ ├── backend
│ │ ├── dist
│ │ │ ├── graphql
│ │ │ │ ├── resolvers.js
│ │ │ │ └── type.js
│ │ │ └── index.js
│ │ ├── src
│ │ │ ├── graphql
│ │ │ │ ├── resolvers.ts
│ │ │ │ └── type.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.tsbuildinfo
│ ├── frontend
│ │ ├── config
│ │ │ └── codegen.ts
│ │ ├── generated
│ │ │ └── graphql.ts
│ │ ├── public
│ │ │ └── vite.svg
│ │ ├── src
│ │ │ ├── assets
│ │ │ │ └── react.svg
│ │ │ ├── App.tsx
│ │ │ ├── main.tsx
│ │ │ ├── service.ts
│ │ │ └── vite-env.d.ts
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ └── packages
│ └── graphql
│ ├── operations
│ │ └── getPost.graphql
│ └── schema
│ ├── post.graphql
│ └── recipe.graphql
├── package-lock.json
├── package.json
└── turbo.json
プロジェクトのトップレベル
.
├── apps # 各アプリケーション (`backend`, `frontend`) を管理
├── packages # 共通パッケージ (`graphql` スキーマ・クエリなど)
├── package.json # プロジェクト全体の依存関係
├── turbo.json # `Turborepo` の設定ファイル
Turborepo
を利用したモノレポ構成apps/
→backend
とfrontend
のコードを分けるpackages/
→ 共通の GraphQL スキーマ (graphql/
) を格納turbo.json
→ Turborepo の設定ファイル
apps/backend/
(バックエンド GraphQL API)
├── backend
│ ├── dist # TypeScript をコンパイルした JavaScript (`.js`) ファイル
│ ├── src # バックエンドの TypeScript (`.ts`) ソースコード
│ ├── package.json # バックエンドの依存関係
│ ├── tsconfig.json # TypeScript の設定
フォルダ / ファイル | 説明 |
---|---|
dist/ | TypeScript をビルドした JavaScript ファイル |
src/ | バックエンドの TypeScript ソースコード |
src/graphql/ | GraphQL のリゾルバー (resolvers.ts), スキーマ (type.ts) |
index.ts | メインのエントリーポイント (Express + Apollo Server) |
package.json | backend の依存関係 |
tsconfig.json | TypeScript の設定 |
apps/frontend/
(フロントエンド React アプリ)
├── frontend
│ ├── config # 設定ファイル (`codegen.ts`)
│ ├── generated # GraphQL Code Generator による型定義
│ ├── public # 静的ファイル (`vite.svg`)
│ ├── src # フロントエンドのソースコード
│ ├── package.json # フロントエンドの依存関係
│ ├── vite.config.ts # `Vite` の設定ファイル
フォルダ / ファイル | 説明 |
---|---|
config/codegen.ts | GraphQL Code Generator の設定ファイル |
generated/graphql.ts | GraphQL Code Generator で生成された型 |
public/ | 静的ファイル (vite.svg) |
src/ | React (Vite) のソースコード |
src/App.tsx | React のメインコンポーネント |
src/service.ts | GraphQL API への通信ロジック |
vite.config.ts | Vite の設定ファイル |
packages/graphql/
(GraphQL の共通パッケージ)
│ └── packages
│ └── graphql
│ ├── operations
│ │ └── getPost.graphql # クエリ (`GetPost`)
│ └── schema
│ ├── post.graphql # `Post` のスキーマ定義
│ └── recipe.graphql # `Recipe` のスキーマ定義
フォルダ / ファイル | 説明 |
---|---|
operations/getPost.graphql | GetPost クエリの定義 |
schema/post.graphql | Post の GraphQL スキーマ (type Post { ... }) |
schema/recipe.graphql | Recipe の GraphQL スキーマ |
さいごに
本記事では、スキーマ駆動開発(SDD)を活用した React + Express(GraphQL)アプリケーションの構築方法 を解説しました。
GraphQL を使うことで、API のスキーマが明確になり、バックエンドとフロントエンドの開発をスムーズに進めることができます。
特に、GraphQL Code Generator を利用して TypeScript の型を自動生成することで、型安全な開発を実現 できました。
これにより、フロントエンドとバックエンドでデータの整合性を確保しつつ、開発の効率化が可能になりました。
✅ まとめ
- スキーマ駆動開発(SDD)とは? → API の仕様をスキーマで定義し、それを基に開発を進めるアプローチ
- メリット → フロントエンド・バックエンドの並行開発が可能、型安全な開発ができる
- 技術スタック → React(Vite)、Express(Apollo Server)、GraphQL
- スキーマから型を自動生成 → GraphQL Code Generator を活用
スキーマ駆動開発を活用すれば、API の変更にも柔軟に対応でき、よりメンテナブルなコードを書くことができます。
今回の内容を参考に、ぜひ実際のプロジェクトでも SDD を取り入れてみてください。
関連する技術ブログ
GraphQL・REST API の堅牢な認可設計:RBAC・ABAC・OAuth 2.0 のベストプラクティス
GraphQL & REST API の堅牢な認可設計を構築する方法とは?RBAC・ABAC の活用、Rate Limiting、API Gateway、監視のベストプラクティスをまとめました。
shinagawa-web.com
キャッシュ戦略完全ガイド:CDN・Redis・API最適化でパフォーマンスを最大化
Webアプリの高速化には、適切なキャッシュ戦略が不可欠。本記事では、CDN(Cloudflare / AWS CloudFront)による静的コンテンツ配信、Redis / Memcached を活用したデータベース負荷軽減、APIレスポンスキャッシュ(GraphQL / REST API)など、キャッシュを駆使してパフォーマンスを向上させる方法を解説。TTL設定、Next.js ISR / SSG のプリフェッチ、クエリキャッシュ(React Query / Apollo Client)、キャッシュヒット率の分析など、実践的なノウハウも紹介します。
shinagawa-web.com
フロントエンド開発に役立つモックサーバー構築:@graphql-tools/mock と Faker を使った実践ガイド
フロントエンド開発を先行させるために、バックエンドが未完成でもモックサーバーを立ち上げる方法を解説。@graphql-tools/mock と Faker を使用して、実際のデータに近い動作をシミュレートします。
shinagawa-web.com
フロントエンドのテスト自動化戦略:Jest・Playwright・MSW を活用したユニット・E2E・API テスト最適化
フロントエンド開発において、品質を担保しながら効率的に開発を進めるためには、適切なテストの自動化が不可欠です。本記事では、Jest や Vitest を活用したユニットテストの導入・強化、React Testing Library や Storybook との統合によるコンポーネントテストの最適化、Playwright / Cypress を用いた E2E テストの拡充について詳しく解説します。さらに、Supertest や MSW を活用した API テストの自動化、Faker / GraphQL Mock によるモックデータの整理、CI/CD パイプラインにおける並列実行やキャッシュ活用による最適化など、テストを効果的に運用するための手法を紹介。また、Codecov / SonarQube によるテストカバレッジの可視化や、フィーチャーフラグを考慮したテスト戦略の策定についても解説し、実践的なアプローチを提案します。テストの信頼性と効率を向上させ、開発プロセスを強化したいフロントエンドエンジニア必見の内容です。
shinagawa-web.com
GraphQL × TypeScript × Zod:Code Generator を活用した型安全な API 開発とスキーマ管理
GraphQL の開発をより型安全かつ効率的に進めるための実践ガイド。GraphQL Code Generator を活用した TypeScript 型定義の自動生成、Zod を用いたバックエンドのレスポンスバリデーション、スキーマ駆動開発(SDL ベース)のフロー確立、変更履歴の管理、フロントエンドでのフェッチ時の型チェック強化などを解説。さらに、GraphQL Playground / Swagger との API ドキュメント自動生成や Mock サーバーを活用した API テストの自動化についても詳しく紹介します。
shinagawa-web.com
Apollo Serverで始めるGraphQL入門:Express & MongoDB連携ガイド
GraphQLとApollo Serverの基本から、MongoDBを活用したデータ操作までを詳しく解説。Expressを使用した効率的なサーバー構築方法を学びましょう。初心者にもわかりやすいハンズオン形式でお届けします。
shinagawa-web.com
ExpressとMongoDBで簡単にWeb APIを構築する方法【TypeScript対応】
本記事では、MongoDB Atlasを活用してREST APIを構築する方法を、初心者の方にも分かりやすいステップで解説します。プロジェクトの初期設定からMongoDBの接続設定、Expressを使用したルートの作成、さらにTypeScriptを用いた型安全な実装まで、実践的なサンプルコードを交えて丁寧に説明します。
shinagawa-web.com
Express(+TypeScript)入門ガイド: Webアプリケーションを素早く構築する方法
Node.jsでWebアプリケーションを構築するための軽量フレームワーク、Expressの基本的な使い方を解説。シンプルなサーバー設定からルーティング、ミドルウェアの活用方法、TypeScriptでの開発環境構築まで、実践的なコード例とともに学べます。
shinagawa-web.com
弊社の技術支援サービス
無駄なコストを削減し、投資対効果を最大化する
クラウド費用の高騰、不要なSaaSの乱立、開発工数の増加――これらの課題に悩んでいませんか?本サービスでは、クラウドコストの最適化、開発効率向上、技術選定の最適化 を通じて、単なるコスト削減ではなく、ROIを最大化する最適解 をご提案します。
shinagawa-web.com
最新技術の導入・検証を支援するPoCサービス
Remix、React Server Components、TypeScript移行、クラウドサービス比較、マイクロサービス、サーバーレス、デザインシステムなど、最新技術のPoC(概念実証)を通じて、最適な技術選定と導入を支援します。貴社の開発課題に合わせた検証・実装で、ビジネスの成長を加速させます。
shinagawa-web.com
開発生産性を最大化するための支援サービス
開発チームの生産性向上、コードの品質管理、インフラの最適化まで、様々な側面からサポートします。コードベースのリファクタリングから、テスト自動化、オンボーディング強化まで、プロジェクトの成功に必要なすべての支援を提供。御社の開発現場が効率的に機能するように、技術的な障害を取り除き、スムーズな開発を実現します。
shinagawa-web.com
開発品質向上支援 – 効率的で安定したプロダクトを実現
フロントエンドからバックエンド、データベースまで、開発プロセス全体を最適化し、安定したプロダクト作りをサポートします。コードレビューの仕組み、型定義の強化、E2Eテスト環境の構築など、開発の各ステップにおけるベストプラクティスを導入することで、より効率的でバグの少ない、そしてユーザー満足度の高いサービス提供を支援します。
shinagawa-web.com
Webアプリのセキュリティ強化支援
Webアプリの脆弱性対策からインフラのセキュリティ強化まで、包括的なセキュリティ支援を提供。OWASP Top 10対策、JWT認証の最適化、APIのアクセス制御、依存パッケージの監査、セキュアコーディングの標準化など、実践的なアプローチで開発現場の安全性を向上させます。
shinagawa-web.com
目次
お問い合わせ