Previously, we mapped the component lifecycle and learned that OnInitializedAsync is the place to load data — today we finally give ClinicApp some data worth loading, by calling a real web API across the internet.

Real apps talk to the outside world

So far, all of ClinicApp's patients have lived inside a PatientService — cozy, but a little lonely. Real apps rarely work like that. They fetch weather from a weather API, products from a store API, appointments from a clinic server. The tool for that conversation in .NET is HttpClient: a built-in class whose whole job is making web requests and handing you back the response.

To practice safely, we'll use JSONPlaceholder — a free, fake online API made exactly for learners like us. It serves pretend users, posts, and comments that we can fetch all day long without signing up for anything. Perfect.

Step 1: register HttpClient

Remember from Part 9 how services get registered in Program.cs before anyone can inject them? HttpClient is no different. In current Blazor (.NET 10), the simplest road is one line:

builder.Services.AddScoped(sp => new HttpClient());

Translation: "whenever a component asks for an HttpClient, hand it one." That's it — the doors to the internet are open.

Note: Bigger production apps usually register clients through IHttpClientFactory (via AddHttpClient), which manages connections more carefully. We're deliberately taking the simple road here — it works great while you learn, and the factory will make perfect sense later.

Step 2: inject it and fetch

In a component, we ask for the client with @inject HttpClient Http, then fetch inside OnInitializedAsync — exactly where Part 10 taught us data-loading belongs:

users = await Http.GetFromJsonAsync<User[]>(
    "https://jsonplaceholder.typicode.com/users");

One line, and a lot happens: the request travels to JSONPlaceholder, the server replies with JSON, and Blazor converts that JSON into an array of C# objects for us. All we need is a class whose shape matches the data:

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public string? Email { get; set; }
}

Notice we didn't write any parsing code. Property names map from the JSON fields automatically — the JSON's "name" flows into our Name, "email" into Email, and fields we don't care about are simply ignored. If JSON itself still feels mysterious, take a quick detour through JSON explained for beginners and come right back.

Tip: GetFromJsonAsync is really two jobs in one line — it downloads the response and parses the JSON into your objects. Doing this manually takes half a page of code. One line. Enjoy it.

When things go wrong (because sometimes they will)

The internet is not a promise. Servers nap, Wi-Fi drops, URLs get typo'd. If our fetch fails and we do nothing, the user sees a broken page — not a great look for a clinic. The fix is a try/catch plus one more piece of state: a friendly error message. Here's the complete page, loading pattern and all:

@page "/directory"
@rendermode InteractiveServer
@using System.Net.Http.Json
@inject HttpClient Http

<h2>Team directory</h2>

@if (loading)
{
    <p>Loading users…</p>
}
else if (errorMessage is not null)
{
    <p>@errorMessage</p>
}
else if (users is not null)
{
    <ul>
        @foreach (var user in users)
        {
            <li><strong>@user.Name</strong> — @user.Email</li>
        }
    </ul>
}

@code {
    private User[]? users;
    private bool loading = true;
    private string? errorMessage;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            users = await Http.GetFromJsonAsync<User[]>(
                "https://jsonplaceholder.typicode.com/users");
        }
        catch (Exception)
        {
            errorMessage = "Sorry — we couldn't load the directory. Please try again in a moment.";
        }

        loading = false;
    }

    public class User
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public string? Email { get; set; }
    }
}

Walk through the three branches and you'll see the whole story of a network request: first we're waiting (loading), then either something went wrong (errorMessage) or we have data (users). If the request throws — no connection, server error, anything — the catch swaps in a human-friendly message instead of a crash. Run it, then turn off your Wi-Fi and refresh to watch the error branch earn its keep.

Wait — was that REST?

Yes! What we just did — sending a GET request to a URL like /users and receiving JSON back — is the everyday rhythm of a REST API. The same pattern extends to creating, updating, and deleting data with PostAsJsonAsync and friends. If you'd like the big picture of how these APIs are organized (with a pizza shop instead of a clinic), read REST APIs explained with a pizza shop — it pairs beautifully with today's code.

And with that, ClinicApp can talk to the outside world. Take a moment — that's a genuinely big milestone. Only one part remains: in Part 12, the series finale, we finally unravel the mystery we've been carrying since day one — what @rendermode InteractiveServer actually means, what the other render modes are, and where your Blazor journey goes from here. See you at the finish line!