ASP.NET Core  

GraphQL with .NET & React | Part 1: Foundations & Library Backend

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

  1. Introduction to GraphQL (This article) - Setup, first query, understanding the basics

  2. Query (Arguments, Aliases & Fragments)

  3. Mutations - Creating, updating, and deleting data

  4. Subscriptions & Real-time - WebSocket-based real-time updates

  5. 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:

  1. Multiple round trips - 3 HTTP requests for one screen

  2. Over-fetching - Each endpoint returns ALL fields, even if you only need 2-3

  3. Under-fetching - You might not get related data you need

  4. 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:

OperationPurposeAnalogy
QueryRead dataLike SELECT in SQL or GET in REST
MutationCreate/Update/Delete dataLike INSERT/UPDATE/DELETE in SQL or POST/PUT/DELETE in REST
SubscriptionReal-time updatesLike 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

  1. .NET 9

  2. 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

  1. 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);
  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

  1. React 18

  2. 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
  1. 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

  1. 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:

  • GET /api/books

  • GET /api/authors/:id (for each book)

  • GET /api/reviews?bookId=:id (for each book)

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

  1. GraphQL is a query language - Not a database, not a framework

  2. Single endpoint - All operations go through /graphql

  3. Client specifies data - Ask for exactly what you need

  4. Strong typing - Schema defines the contract

  5. Three operation types - Query (read), Mutation (write), Subscription (real-time)

  6. HotChocolate - .NET library that creates GraphQL servers

  7. Apollo Client - React library that consumes GraphQL APIs

  8. Automatic caching - Apollo stores responses for better performance

  9. 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

  1. Run the backend and explore the schema in Banana Cake Pop

  2. Write a query to fetch only book titles and prices

  3. Write a query that includes author biography

  4. Try requesting a field that doesn't exist - see what error you get

  5. 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)