Practical Schema-Driven Development: Efficient API Design with React × Express × GraphQL
Introduction
In recent years, Schema-Driven Development (SDD) has been attracting attention as an approach to streamline frontend and backend development.
In SDD using GraphQL, you first define the GraphQL schema and then implement the backend and frontend based on that schema.
This clarifies the API specifications and makes it possible to develop the frontend and backend in parallel.
In this article, we will explain how to practice schema-driven development using React (Vite) + Express (Apollo Server).
We will also achieve type-safe development by automatically generating TypeScript types from GraphQL schemas.
By reading this article, you will learn:
- Benefits of schema-driven development
- Building a GraphQL API using React (Vite) + Express (Apollo Server)
- Automatically generating TypeScript types from GraphQL schemas
- How to connect the frontend and backend
What is Schema-Driven Development?
Schema-Driven Development (SDD) is a development methodology in which API specifications are designed and implemented based on GraphQL schema definitions (SDL: Schema Definition Language).
With traditional REST APIs, you needed to create a separate API specification document and share it between the frontend and backend. However, in GraphQL, the schema itself becomes the API specification, which makes the development process smoother.
Benefits
-
Clear specifications
Since the schema itself is the API specification, it reduces the likelihood of misunderstandings about the specs between the frontend and backend. -
Automatic documentation
Using tools like GraphQL Playground or Apollo Studio, you can automatically generate documentation from schema definitions, making it easier for API consumers to understand and use the API. -
Easy detection of changes
When there are changes to the schema, consistency checks with the implementation are more likely to occur automatically, making the impact of changes clearer.
By following this process, you can improve the overall efficiency of the team and achieve smooth communication and project management.
Goal of This Tutorial
We will proceed with the implementation in the following steps:
- Create a GraphQL schema definition
- Load the schema definition on the server and start a GraphQL server
- Generate TypeScript type definitions from the GraphQL schema and GraphQL queries
- Fetch data from the server via GraphQL from the frontend
In this tutorial, we will use post data as a sample,
and in the end we will display the post titles and author information in the browser.
The code implemented in this tutorial is stored in the following repository, so please refer to it as well.
Setup
First, we start with the overall project setup.
mkdir Documents/workspace/schema-driven-development-react-express-graphql
cd Documents/workspace/schema-driven-development-react-express-graphql
npm init -y
Install Turborepo so we can use it.
npm install turbo -D
Turborepo is a tool for managing monorepo structures, mainly used in JavaScript and TypeScript projects. A monorepo is an approach where multiple projects or packages are managed within a single repository. Turborepo is a tool designed to efficiently handle such monorepos.
Next, configure Turborepo.
You can define tasks for things like build and starting in development mode.
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Next, configure the root-level 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: A setting for building a monorepo. A monorepo is an approach where multiple packages or applications are managed in a single repository.
By using workspaces, you can efficiently manage different packages (applications and libraries) within the same repository, share dependencies, speed up installation, and improve development efficiency.
Creating the Frontend (React + Vite)
We will start with the frontend configuration.
mkdir apps
cd apps
npm create vite@latest frontend
When building the frontend with Vite, you will be asked a few questions; configure them as follows:
Select a framework: React
Select a variant: TypeScript + SWC
Next, configure the scoped package.
{
- "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"
}
}
Now that we have downloaded the necessary files for Vite and React, install the packages.
cd apps/frontend
npm install
After installation, start the app to make sure it works.
npm run dev
If you can access http://localhost:5173/, the frontend setup is complete.
Creating the Backend (Express.js)
Next, configure Express as the backend.
First, install the packages.
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
Then configure the scoped package and 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
}
}
Now that we have an environment where Express can run, create the startup file.
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}`);
});
- When you send a GET request to
/, the stringHello from Express!is returned.
After creating the file, define the startup script in package.json.
- "main": "index.js",
+ "main": "index.ts",
"scripts": {
+ "dev": "ts-node src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
Check that Express can start.
npm run dev
Server is running on http://localhost:4000
Once it is running, check the response with curl.
curl http://localhost:4000
Hello from Express!
This completes the Express setup.
Connecting the Frontend and Backend
We will use the fetch API to access Express and display the GET response Hello from Express! in the browser.
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;
The default CSS is still there and affects the layout, so remove it for now.
rm apps/frontend/src/index.css
rm apps/frontend/src/App.css
Also update the files that import the 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>,
)
Now that everything is configured, start React and Express via Turborepo.
Run the following command at the project root.
npm run dev
We have managed the frontend and backend repositories with Turborepo and connected both sides.
From here, we will move on to the main topic of this article: “Schema-Driven Development.”
Creating the Schema Definition
To make the GraphQL definitions accessible from both the frontend and backend, we will place them in a shared directory in the monorepo.
mkdir apps/packages
mkdir apps/packages/graphql
mkdir apps/packages/graphql/schema
This GraphQL schema defines an API that handles blog posts (Post) and their authors (Author).
type Author {
id: Int!
firstName: String!
lastName: String!
}
type Post {
id: Int!
title: String!
author: Author
}
type Query {
posts: [Post] # Retrieve all posts
post(id: ID!): Post # Retrieve a single post
}
-
Authoris the author who wrote the post (Post)id: Int!→ Unique ID of the author (!means the field is required)firstName: String!→ Author’s first name (required)lastName: String!→ Author’s last name (required)
-
Postis the blog post dataid: Int!→ Unique ID of the post (required)title: String!→ Title of the post (required)author: Author→ Author of the post (Author type)
-
By defining
author: Author, you can retrieve the author (Author) from a post (Post). -
Endpoints that the client can call
posts: [Post]→ Retrieve all postspost(id: ID!): Post→ Retrieve a specific post by IDid: ID!is GraphQL’sIDtype (usually storesStringorInt)
Backend Implementation
To start a GraphQL server with Express, install a few libraries.
npm i @apollo/server graphql graphql-tag graphql-tools/load-files
npm i -D @types/express-serve-static-core
| Package name | Description |
|---|---|
| @apollo/server | Apollo Server (a server library that provides a GraphQL API) |
| graphql | Core GraphQL library (parsing schemas, validation, etc.) |
| graphql-tag | Utility for parsing GraphQL queries (gql templates) |
| graphql-tools/load-files | Library for loading GraphQL schemas and resolvers from files |
| @types/express-serve-static-core | Type definitions for express (including internal features like serve-static) |
Next, define the resolvers.
// Dummy data
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: {
// Retrieve all posts
posts: () => posts,
// Retrieve a specific post by ID
post: (_parent: any, args: { id: number }) =>
posts.find((post) => post.id === args.id),
},
};
-
Resolver corresponding to GraphQL
Query.posts- Returns all posts (the
postsarray) - Works easily without a database
- Returns all posts (the
-
Resolver corresponding to GraphQL
Query.post(id: ID!)- Searches for the post whose
post.idmatchesargs.id - Returns the matching
post, orundefinedif not found - If there is no data, it may become
null, so the client side needs to handle that
- Searches for the post whose
Next, configure loading the GraphQL schema (.graphql file) and passing the schema definition to 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 };
- Parses GraphQL queries and schemas as
gql(GraphQL tag)graphql-tagis a library that parses GraphQL schemas read as strings using thegqltag- By using
gql, you can correctly pass the schema to 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();
-
Libraries
express→ Framework for creating HTTP serverscors→ Middleware to allow CORS (cross-origin requests)@apollo/server→ Apollo Server (library that provides a GraphQL API)expressMiddleware→ Function to integrate Apollo Server as middleware in Expressresolvers→ GraphQL resolvers (functions that handle queries)typeDefs→ GraphQL schema (defines the structure of the data)
-
Create the GraphQL server (Apollo Server)
- Pass typeDefs (schema) and resolvers (data fetching logic) to Apollo Server
- This
serverprovides the GraphQL API endpoint
-
Integrate the GraphQL server (server) into Express and start it
server.start()→ Start Apollo Serverapp.use('/graphql', ...)→ Create the/graphqlendpointcors()→ Allow CORS (do not restrict requests from the browser)express.json()→ Parse JSON requestsexpressMiddleware(server)→ Register Apollo Server as Express middleware
app.listen(PORT, () => { ... })→ Start the server on the specified port (4000)
After finishing the configuration, start the Express server and check if you get a response with curl.
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ posts { id title } }"}' | jq
If the dummy data defined in the resolvers is returned as a response, the server-side setup is complete.
{
"data": {
"posts": [
{
"id": 1,
"title": "GraphQL Introduction"
},
{
"id": 2,
"title": "Advanced GraphQL"
},
{
"id": 3,
"title": "GraphQL with Apollo"
}
]
}
}
Bonus: Filtering by title
We will add a setting to filter by title when executing the query.
export const resolvers = {
Query: {
- // Retrieve all posts
+ // Retrieve all posts with optional title filtering
- 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;
},
// Retrieve a specific post by ID
post: (_parent: any, args: { id: number }) =>
posts.find((post) => post.id === args.id),
},
};
- If
findTitleis provided, it returns posts whose titles partially match it. - If
findTitleis not provided, it returns all posts.
Set findTitle as an argument in the query definition.
type Query {
- posts: [Post]
+ posts(findTitle: String): [Post] # Allow filtering by title
post(id: ID!): Post # Retrieve a single 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"
}
]
}
}
Only titles containing the word “Apollo” are included in the response.
Automatically Generating TypeScript Types from the Schema
Next, to use TypeScript types on the frontend, we will generate them from the GraphQL definitions.
Install GraphQL Code Generator:
npm i -D @graphql-codegen/cli
Create a configuration file for GraphQL Code Generator (@graphql-codegen/cli) and configure it to automatically generate typed TypeScript code from GraphQL schemas and queries.
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: Specifies the path to the GraphQL schema files../packages/graphql/schema/**/*.graphql- Targets all
.graphqlfiles underpackages/graphql/schema/ **/*.graphqlrecursively finds all GraphQL schemas
- Targets all
- The schema includes type definitions such as
type Query,type Mutation,type Post, etc.
-
generates: Configures which file to generate types into"generated/graphql.ts"→ Generates TypeScript types intogenerated/graphql.tsplugins→ Specifies which plugins to use
-
plugins: Plugins that generate types from GraphQL schemas and queries'typescript'→ Generates TypeScript types from GraphQL schemas (schema.graphql)'typescript-operations'→ Generates TypeScript types from GraphQL queries (.graphql)
{
"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"
},
- Configure the command to run
graphql-codegen.
npm run codegen
- TypeScript types will be generated in
generated/graphql.ts.
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'];
};
Now we can generate TypeScript type definitions from GraphQL schema definitions.
Even if you create multiple GraphQL schema definitions, you can generate TypeScript type definitions for all of them at once.
type Recipe {
id: Int!
title: String!
imageUrl: String
}
type Query {
recipe(id: Int!): Recipe!
}
Run it again:
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'];
};
You can see that Query now has both posts and recipe, and they are handled together.
Frontend Implementation
First, create the GraphQL query.
This GraphQL query GetPosts retrieves all posts (posts) and, for each post, fetches its id, title, and author information.
query GetPosts {
posts {
id
title
author {
firstName
lastName
}
}
}
Add settings to config.ts so that GraphQL queries can also be used for type generation.
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;
Then the following GetPostsQuery will be generated.
This is the return type of 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 is one of the GraphQL Code Generator plugins. It generates TypeScript types and client code suitable for graphql-request from GraphQL schemas and queries.
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;
After adding this to the Code Generator config and regenerating the types, the following will be added:
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
}
Key points:
graphql-requestis a simple GraphQL client library used to send HTTP requests to a GraphQL API.getSdkis an API client automatically generated byGraphQL Code Generator.- Using
graphql-code-generator, you can convert GraphQL queries into TypeScript-typed functions (getSdk). - By using
getSdk, you can easily execute GraphQL queries and fetch data in a type-safe way.
- Using
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;
GetPostsQuerycan be used as the return type ofgetPosts.
Checking the result, the post titles are displayed, but the authors are not.
Let’s check the resolvers again.
It seems we forgot to set the author information when returning the list of posts.
export const resolvers = {
Query: {
// Retrieve all posts with optional title filtering
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;
},
// Retrieve a specific post by ID
post: (_parent: any, args: { id: number }) =>
posts.find((post) => post.id === args.id),
},
};
We will fix it so that author information is set.
For now, we will also handle this with dummy data.
// Dummy data
+ 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: {
// Retrieve all posts with optional title filtering + add `author`
posts: (_parent: any, args: { findTitle?: string }) => {
+ let filteredPosts = posts;
if (args.findTitle) {
filteredPosts = filteredPosts.filter((post) =>
post.title.toLowerCase().includes(args.findTitle!.toLowerCase())
);
}
+ // Convert `authorId` to `author`
+ return filteredPosts.map((post) => ({
+ ...post,
+ author: authors.find((author) => author.id === post.authorId) || null,
+ }));
},
// Retrieve a specific post by ID + add `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,
};
},
},
};
Checking again in the browser, the author information is now correctly set.
Directory Structure
Finally, let’s check the directory structure and review the overall picture.
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
Project Top Level
.
├── apps # Manages each application (`backend`, `frontend`)
├── packages # Shared packages (GraphQL schemas, queries, etc.)
├── package.json # Project-wide dependencies
├── turbo.json # `Turborepo` configuration file
- Monorepo structure using
Turborepo apps/→ Separate code forbackendandfrontendpackages/→ Store shared GraphQL schemas (graphql/)turbo.json→ Turborepo configuration file
apps/backend/ (Backend GraphQL API)
├── backend
│ ├── dist # Compiled JavaScript (`.js`) from TypeScript
│ ├── src # Backend TypeScript (`.ts`) source code
│ ├── package.json # Backend dependencies
│ ├── tsconfig.json # TypeScript configuration
| Folder / File | Description |
|---|---|
| dist/ | JavaScript files built from TypeScript |
| src/ | Backend TypeScript source code |
| src/graphql/ | GraphQL resolvers (resolvers.ts), schema loader (type.ts) |
| index.ts | Main entry point (Express + Apollo Server) |
| package.json | Backend dependencies |
| tsconfig.json | TypeScript configuration |
apps/frontend/ (Frontend React App)
├── frontend
│ ├── config # Configuration files (`codegen.ts`)
│ ├── generated # Type definitions generated by GraphQL Code Generator
│ ├── public # Static files (`vite.svg`)
│ ├── src # Frontend source code
│ ├── package.json # Frontend dependencies
│ ├── vite.config.ts # `Vite` configuration file
| Folder / File | Description |
|---|---|
| config/codegen.ts | GraphQL Code Generator configuration file |
| generated/graphql.ts | Types generated by GraphQL Code Generator |
| public/ | Static files (vite.svg) |
| src/ | React (Vite) source code |
| src/App.tsx | Main React component |
| src/service.ts | GraphQL API communication logic |
| vite.config.ts | Vite configuration file |
packages/graphql/ (Shared GraphQL Package)
│ └── packages
│ └── graphql
│ ├── operations
│ │ └── getPost.graphql # Query (`GetPost`)
│ └── schema
│ ├── post.graphql # `Post` schema definition
│ └── recipe.graphql # `Recipe` schema definition
| Folder / File | Description |
|---|---|
| operations/getPost.graphql | GetPost query definition |
| schema/post.graphql | GraphQL schema for Post (type Post { ... }) |
| schema/recipe.graphql | GraphQL schema for Recipe |
Conclusion
In this article, we explained how to build a React + Express (GraphQL) application using Schema-Driven Development (SDD).
By using GraphQL, the API schema becomes explicit, making it easier to develop the backend and frontend smoothly.
In particular, by using GraphQL Code Generator to automatically generate TypeScript types, we achieved type-safe development.
This allows you to ensure data consistency between the frontend and backend while improving development efficiency.
✅ Summary
- What is Schema-Driven Development (SDD)? → An approach where you define API specifications as schemas and develop based on them
- Benefits → Enables parallel development of frontend and backend, and type-safe development
- Tech stack → React (Vite), Express (Apollo Server), GraphQL
- Automatically generating types from schemas → Using GraphQL Code Generator
By leveraging schema-driven development, you can flexibly handle API changes and write more maintainable code.
Please use this tutorial as a reference and try adopting SDD in your own projects.
The code implemented in this tutorial is stored in the following repository, so please refer to it as well.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Robust Authorization Design for GraphQL and REST APIs: Best Practices for RBAC, ABAC, and OAuth 2.0
2024/05/13Complete Cache Strategy Guide: Maximizing Performance with CDN, Redis, and API Optimization
2024/03/07Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability
2024/11/22How to Easily Build a Web API with Express and MongoDB [TypeScript Compatible]
2024/12/09Express (+ TypeScript) Beginner’s Guide: How to Quickly Build Web Applications
2024/12/07Complete Guide to Refactoring React: Improve Your Code with Modularization, Render Optimization, and Design Patterns
2025/01/13Test Automation with Jest and TypeScript: A Complete Guide from Basic Setup to Writing Type-Safe Tests
2023/09/13ESLint / Prettier Introduction Guide: Thorough Explanation from Husky, CI/CD Integration, to Visualizing Code Quality
2024/02/12


