Series: GraphQL with .NET & React, From Zero to Production
Welcome to this comprehensive GraphQL tutorial series! By the end of these 5 articles, you'll have a solid understanding of GraphQL and hands-on experience building a full-stack application with React and .NET.
Get your code here: GitHub
This is the series that takes you from GraphQL basics to advanced patterns.
What We're Building
Throughout this series, we'll build a Library Management System with the following features:
Browse and search books
View author profiles and their books
Add reviews with real-time updates
Full CRUD operations for books, authors, and reviews
Pagination, filtering, and sorting
Table of Contents for This Series
Introduction to GraphQL (This article) - Setup, first query, understanding the basics
Query (Arguments, Aliases & Fragments)
Mutations - Creating, updating, and deleting data
Subscriptions & Real-time - WebSocket-based real-time updates
Advanced Topics - Pagination, error handling, authentication patterns
What is GraphQL?
GraphQL (Graph Query Language) is a query language for APIs developed by Facebook in 2012 and open-sourced in 2015. It's not a database or a programming language - it's a specification for how clients can request data from servers.
The Problem GraphQL Solves
Imagine you're building a mobile app that shows a book's details. With a traditional REST API, you might need:
GET /api/books/1 → Returns book data (but maybe too much!)
GET /api/books/1/author → Returns author data (another round trip!)
GET /api/books/1/reviews → Returns reviews (yet another request!)
Problems with this approach:
Multiple round trips - 3 HTTP requests for one screen
Over-fetching - Each endpoint returns ALL fields, even if you only need 2-3
Under-fetching - You might not get related data you need
Rigid structure - Backend decides what data you get
How GraphQL Solves It
With GraphQL, you ask for exactly what you need in ONE request:
query {
bookById(id: 1) {
title
price
author {
name
}
reviews {
rating
}
}
}
Benefits:
One request - Get everything in a single HTTP call
No over-fetching - Only get the fields you ask for
No under-fetching - Get related data in the same request
Client-driven - Frontend decides what data it needs
Core Concepts of GraphQL
Before we write any code, let's understand the three main operation types in GraphQL:
| Operation | Purpose | Analogy |
|---|
| Query | Read data | Like SELECT in SQL or GET in REST |
| Mutation | Create/Update/Delete data | Like INSERT/UPDATE/DELETE in SQL or POST/PUT/DELETE in REST |
| Subscription | Real-time updates | Like a live feed - server pushes data when something changes |
The GraphQL Schema
Every GraphQL API has a schema - a contract that defines:
What data types exist (Book, Author, Review, etc.)
What queries you can make
What mutations you can perform
What subscriptions are available
Think of it like a menu at a restaurant - it tells you exactly what you can order.
Technology Stack Overview
Let's understand each technology we'll use and why.
Backend Technologies
.NET 9
HotChocolate - Our GraphQL Server
HotChocolate is a GraphQL server library for .NET. Think of it as the translator between GraphQL and your C# code.
What it does:
Receives GraphQL queries from clients
Validates them against your schema
Executes the appropriate C# code
Returns data in GraphQL format
Entity Framework Core - Our ORM
Entity Framework Core (EF Core) is an Object-Relational Mapper (ORM).
What is an ORM? Instead of writing raw SQL queries, you work with C# objects. EF Core translates your C# code into SQL automatically.
// Instead of: "SELECT * FROM Books WHERE Id = 1"
// You write:
var book = context.Books.Find(1);
SQLite - Our Database
SQLite is a lightweight, file-based database. Unlike MySQL or PostgreSQL, it doesn't need a server - the entire database is a single file.
Frontend Technologies
React 18
TypeScript
TypeScript is JavaScript with types. It catches errors at compile time rather than runtime.
// JavaScript - no error until runtime
function add(a, b) { return a + b; }
add("hello", 5); // Returns "hello5" - probably not what you wanted!
// TypeScript - error at compile time
function add(a: number, b: number): number { return a + b; }
add("hello", 5); // ❌ Error: Argument of type 'string' is not assignable
Apollo Client - Our GraphQL Client
Apollo Client is a state management library that makes it easy to work with GraphQL.
What it does:
Sends GraphQL queries to your server
Caches responses automatically (so repeated queries are instant)
Updates your UI when data changes
Handles loading and error states
Vite - Our Build Tool
Vite (French for "fast") is a build tool that makes development lightning quick.
What it does:
Compiles your TypeScript/React code
Runs a development server with hot reload
Bundles your code for production
Project Setup
Now that you understand what each piece does, let's set it up!
Backend Project Structure
backend/
├── Models/ # C# classes representing our data
│ ├── Author.cs
│ ├── Book.cs
│ ├── Review.cs
│ ├── Category.cs
│ └── BookCategory.cs
├── Data/ # Database configuration
│ ├── LibraryDbContext.cs # EF Core database context
│ └── DataSeeder.cs # Populates initial data
├── GraphQL/ # All GraphQL-related code
│ ├── Types/ # GraphQL type definitions
│ ├── Queries/ # Read operations
│ ├── Mutations/ # Write operations
│ └── Subscriptions/ # Real-time updates
├── Program.cs # Application entry point
└── appsettings.json # Configuration
Understanding Our Data Model
Before writing code, let's understand our domain:
┌─────────┐ ┌─────────┐ ┌──────────┐
│ Author │──1:n──│ Book │──n:m──│ Category │
└─────────┘ └────┬────┘ └──────────┘
│
1:n
│
┌────┴────┐
│ Review │
└─────────┘
1:n = One to Many (One author has many books)
n:m = Many to Many (Books can have multiple categories, categories have multiple books)
Creating the Book Model
Let's look at our Book model and understand each part:
// Models/Book.cs
namespace LibraryApi.Models;
public class Book
{
// Primary key - unique identifier for each book
public int Id { get; set; }
// 'required' means this field must have a value
public required string Title { get; set; }
// '?' makes this nullable - description is optional
public string? Description { get; set; }
public string? Isbn { get; set; }
public int PublishedYear { get; set; }
public string? Genre { get; set; }
// 'decimal' is used for money - more precise than float
public decimal Price { get; set; }
public int PageCount { get; set; }
// Default value - books start as available
public bool IsAvailable { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Foreign key - links to the Author table
public int AuthorId { get; set; }
// Navigation property - EF Core uses this to load related Author
public Author? Author { get; set; }
// Collection navigation - a book has many reviews
public ICollection<Review> Reviews { get; set; } = new List<Review>();
}
Configuring HotChocolate
Here's how we set up the GraphQL server in Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Register our database context
// This tells EF Core to use SQLite and where to store the file
builder.Services.AddDbContext<LibraryDbContext>(options =>
options.UseSqlite("Data Source=library.db"));
// Configure the GraphQL server
builder.Services
.AddGraphQLServer() // Add HotChocolate
.AddQueryType<Query>() // Register our Query class
.AddMutationType<Mutation>() // Register our Mutation class
.AddSubscriptionType<Subscription>() // Register Subscriptions
.AddType<AuthorType>() // Custom type configurations
.AddType<BookType>()
.AddType<ReviewType>()
.AddType<CategoryType>()
.AddFiltering() // Enable automatic filtering
.AddSorting() // Enable automatic sorting
.AddProjections() // Enable field selection optimization
.AddInMemorySubscriptions(); // Store subscription state in memory
var app = builder.Build();
// Enable WebSockets (required for subscriptions)
app.UseWebSockets();
// Map the /graphql endpoint
app.MapGraphQL();
app.Run();
What each method does:
AddGraphQLServer() - Initializes HotChocolate
AddQueryType<Query>() - Tells HotChocolate where to find query resolvers
AddFiltering() - Auto-generates filter arguments (like where: { price: { gt: 10 } })
AddInMemorySubscriptions() - Enables real-time features
Understanding the GraphQL Schema
When you run the backend and visit http://localhost:5000/graphql, you'll see Banana Cake Pop - HotChocolate's built-in GraphQL IDE (like Postman, but for GraphQL).
What is a Schema?
The schema is auto-generated from your C# code. It defines:
# A type definition - describes the shape of a Book
type Book {
id: Int! # Int = integer, ! = required (non-null)
title: String! # String = text
description: String # No ! means it can be null
isbn: String
publishedYear: Int!
genre: String
price: Decimal! # Decimal for precise numbers
pageCount: Int!
isAvailable: Boolean!
author: Author # Related type - can fetch author data
reviews: [Review!]! # Array of Review, array itself is required
categories: [Category!]!
averageRating: Float # Calculated field
reviewCount: Int!
}
Reading the syntax:
Int, String, Boolean, Float - Scalar (simple) types
! - Non-nullable (will never be null)
[Type] - Array of that type
[Type!]! - Non-null array of non-null items
Your First GraphQL Query
Now let's actually query some data!
Basic Query Syntax
query {
books {
nodes {
id
title
publishedYear
price
}
}
}
Breaking it down:
query - The operation type (we're reading data)
books - The field we're querying (defined in our Query class)
nodes - HotChocolate's pagination wrapper (contains the actual items)
{ id, title, ... } - The fields we want returned
Response Format
GraphQL always returns JSON with this structure:
{
"data": {
"books": {
"nodes": [
{
"id": 1,
"title": "1984",
"publishedYear": 1949,
"price": 15.99
},
{
"id": 2,
"title": "Animal Farm",
"publishedYear": 1945,
"price": 12.99
}
]
}
}
}
Notice:
Response mirrors query structure exactly
Only requested fields are returned
No extra data you didn't ask for!
Query with Nested Data
The real power - fetching related data in one request:
query {
books {
nodes {
id
title
author { # Nested object
name
country
}
reviews { # Nested array
rating
reviewerName
}
}
}
}
This single query replaces:
Frontend Setup
Project Structure
frontend/
├── src/
│ ├── graphql/ # GraphQL-related code
│ │ ├── client.ts # Apollo Client configuration
│ │ ├── queries.ts # Query definitions
│ │ ├── mutations.ts # Mutation definitions
│ │ └── subscriptions.ts
│ ├── components/ # React components
│ ├── types/ # TypeScript interfaces
│ └── App.tsx # Main application
├── package.json # Dependencies
└── vite.config.ts # Vite configuration
Setting Up Apollo Client
// src/graphql/client.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
// HttpLink tells Apollo where to send queries
const httpLink = new HttpLink({
uri: 'http://localhost:5000/graphql', // Our backend URL
});
// Create the Apollo Client
export const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(), // Stores query results
});
What each piece does:
HttpLink - Handles HTTP communication with the server
InMemoryCache - Stores responses so repeated queries are instant
ApolloClient - The main object that manages everything
Using Apollo Provider
Wrap your app to make Apollo available everywhere:
// App.tsx
import { ApolloProvider } from '@apollo/client';
import { client } from './graphql/client';
function App() {
return (
<ApolloProvider client={client}>
<YourComponents />
</ApolloProvider>
);
}
Your First React Query
Apollo provides the useQuery hook:
import { useQuery, gql } from '@apollo/client';
// Define the query using gql template tag
const GET_BOOKS = gql`
query GetBooks {
books {
nodes {
id
title
author {
name
}
price
}
}
}
`;
function BookList() {
// useQuery executes the query and returns state
const { loading, error, data } = useQuery(GET_BOOKS);
// Handle loading state
if (loading) return <p>Loading...</p>;
// Handle error state
if (error) return <p>Error: {error.message}</p>;
// Render the data
return (
<div>
{data.books.nodes.map((book) => (
<div key={book.id}>
<h3>{book.title}</h3>
<p>by {book.author?.name}</p>
<p>${book.price}</p>
</div>
))}
</div>
);
}
What useQuery returns:
loading - Boolean, true while fetching
error - Error object if something went wrong
data - The actual response data
Running the Application
Start the Backend
cd backend
dotnet run
Visit http://localhost:5000/graphql to explore the API with Banana Cake Pop.
Start the Frontend
cd frontend
npm install
npm run dev
Visit http://localhost:5173 to see the React app.
Key Takeaways
GraphQL is a query language - Not a database, not a framework
Single endpoint - All operations go through /graphql
Client specifies data - Ask for exactly what you need
Strong typing - Schema defines the contract
Three operation types - Query (read), Mutation (write), Subscription (real-time)
HotChocolate - .NET library that creates GraphQL servers
Apollo Client - React library that consumes GraphQL APIs
Automatic caching - Apollo stores responses for better performance
Get your code here: GitHub
What's Next?
In Part 2: we'll explore:
Query arguments and variables
Aliases for querying the same field multiple times
Fragments for reusable query pieces
The @include and @skip directives
HotChocolate's automatic filtering and sorting
Exercises
Run the backend and explore the schema in Banana Cake Pop
Write a query to fetch only book titles and prices
Write a query that includes author biography
Try requesting a field that doesn't exist - see what error you get
Compare the response size when requesting 2 fields vs 10 fields
See you!
This is the one series to take you from GraphQL basics to advanced patterns.
Part 2: Query (Arguments, Aliases & Fragments)
Part 3: Mutations (Create, Update & Delete)
Part 4: Real-Time Data with web sockets
Part 5: Advanced Patterns (Pagination, Errors, JWT & Testing)