π Lesson 5.1: Calling Web APIs with HttpClient
This is where it all comes together. You'll fetch live data from the internet with HttpClient β using the async you learned in 4.3 and the JSON from 4.2 β to turn a web response into real C# objects.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a web API and an HTTP request are
- Send async GET requests with
HttpClient - Deserialize a JSON response straight into objects with
GetFromJsonAsync - Check status codes and handle network errors
- Reuse a single
HttpClientcorrectly
Estimated Time: 75 minutes
Project: Fetch and display data from a public web API.
In This Lesson
What Is a Web API?
A web API is a service on the internet that programs (not people) can call to get or send data. Your code sends an HTTP request to a URL; the server sends back an HTTP response, very often as JSON.
π Definitions
HTTP: the protocol browsers and apps use to talk to servers. A request has a method β GET (fetch data), POST (send data), and others.
Web API / REST: a server exposing data at URLs (called endpoints), usually exchanging JSON.
HttpClient"] -->|"GET request (URL)"| B["Web server
(the API)"] B -->|"HTTP response (JSON)"| A A --> C["Deserialize JSON
into objects"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style C fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
In .NET, the class for making these calls is HttpClient (in System.Net.Http). Every method is asynchronous β network calls are exactly the slow, waiting work that async/await was made for.
Your First Request
The simplest call fetches a URL's content as a string. Since it's async, we await it (Lesson 4.3):
using System.Net.Http;
HttpClient client = new HttpClient();
string json = await client.GetStringAsync("https://api.example.com/products");
Console.WriteLine(json); // the raw JSON text the server returned
That gives you the response body as text. You could then hand it to JsonSerializer.Deserialize from Lesson 4.2 β but there's a shortcut that does both steps at once, coming next.
π‘ A public API to practice with
Free "fake" APIs like https://jsonplaceholder.typicode.com return sample JSON for learning β for example /todos/1 returns a single to-do item. We'll use it in the exercise so you can run real requests safely.
JSON Straight into Objects
The method GetFromJsonAsync<T> (from System.Net.Http.Json) fetches a URL and deserializes the JSON response into your type in one call β combining Lessons 4.2 and 4.3 beautifully:
using System.Net.Http.Json;
record Todo(int Id, int UserId, string Title, bool Completed);
HttpClient client = new HttpClient();
Todo? todo = await client.GetFromJsonAsync<Todo>(
"https://jsonplaceholder.typicode.com/todos/1");
Console.WriteLine($"{todo?.Id}: {todo?.Title} (done: {todo?.Completed})");
Fetching a whole list is just as easy β deserialize into a List<T>:
List<Todo>? todos = await client.GetFromJsonAsync<List<Todo>>(
"https://jsonplaceholder.typicode.com/todos");
Console.WriteLine($"Fetched {todos?.Count} todos.");
// Now use LINQ (Lesson 2.3) on live data!
var completed = todos?.Where(t => t.Completed).Count();
Console.WriteLine($"{completed} are completed.");
β Everything you've learned, working together
Look at that last example: an async network call (4.3) returns JSON deserialized (4.2) into records (3.1) in a generic List<T> (1.2), which you then query with LINQ (2.3). This is real, modern C#.
π‘ Property name matching
The JSON here uses camelCase (userId), and your record uses PascalCase (UserId). System.Text.Json matches names case-insensitively by default, so it just works β but this is why the naming options from Lesson 4.2 matter when APIs are involved.
Status Codes & Errors
Every HTTP response carries a status code that says how it went. You'll want to recognize the common ranges:
| Range | Meaning | Examples |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created |
| 4xx | Client error (your request was wrong) | 404 Not Found, 401 Unauthorized |
| 5xx | Server error | 500 Internal Server Error |
For finer control, use GetAsync, which returns a response object you can inspect before reading the body:
HttpResponseMessage response = await client.GetAsync(url);
if (response.IsSuccessStatusCode) // true for any 2xx
{
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}
else
{
Console.WriteLine($"Request failed: {(int)response.StatusCode} {response.StatusCode}");
}
Two kinds of things can go wrong, and robust code handles both (using Lesson 1.1's exception skills):
try
{
Todo? todo = await client.GetFromJsonAsync<Todo>(url);
Console.WriteLine(todo?.Title);
}
catch (HttpRequestException ex) // network failure, DNS, non-success status
{
Console.WriteLine($"Network error: {ex.Message}");
}
catch (TaskCanceledException) // timeout
{
Console.WriteLine("The request timed out.");
}
β οΈ The network is never guaranteed
Unlike local method calls, a web request can fail for reasons outside your control β no connection, a down server, a timeout. Always wrap API calls in try/catch (HttpRequestException). (Note: GetFromJsonAsync and EnsureSuccessStatusCode() throw HttpRequestException automatically on non-success codes.)
Reusing HttpClient
Here's a crucial real-world detail. Although HttpClient is IDisposable, you should not create a new one for every request. Doing so in a loop can exhaust the operating system's network connections (a well-known bug called socket exhaustion).
β οΈ Don't do this
// β A new client per request β can exhaust sockets
foreach (var url in urls)
{
using var client = new HttpClient(); // BAD in a loop
var data = await client.GetStringAsync(url);
}
β Do this instead
Create one HttpClient and reuse it for many requests. It's designed to be shared and is safe for concurrent use:
// β
One shared client, reused
HttpClient client = new HttpClient();
foreach (var url in urls)
{
var data = await client.GetStringAsync(url);
}
In larger apps, the recommended approach is IHttpClientFactory (used with dependency injection), which manages client lifetimes for you. For our console programs, a single shared instance is exactly right.
π‘ Setting a base address and headers
You can configure a client once and reuse it β a base URL, a default timeout, headers like an API key:
HttpClient client = new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
Timeout = TimeSpan.FromSeconds(10)
};
// Then use relative paths:
var todo = await client.GetFromJsonAsync<Todo>("todos/1");
Exercise & Quiz
ποΈ Exercise: A Live To-Do Fetcher
Objective: Fetch real JSON data from a public API and process it with LINQ.
Instructions:
- Create a new project called
ApiClient. - Define
record Todo(int Id, int UserId, string Title, bool Completed);. - Create one shared
HttpClient. - Use
GetFromJsonAsync<List<Todo>>to fetchhttps://jsonplaceholder.typicode.com/todos. - Print how many todos there are, and β using LINQ β how many are completed and the title of the first incomplete one.
- Wrap everything in
try/catch (HttpRequestException).
Starter Code:
using System.Net.Http.Json;
record Todo(int Id, int UserId, string Title, bool Completed);
HttpClient client = new HttpClient();
// TODO: try/catch; fetch List<Todo>; print count, completed count, first incomplete title
π‘ Hint
var todos = await client.GetFromJsonAsync<List<Todo>>("https://jsonplaceholder.typicode.com/todos");. Then todos.Count(t => t.Completed) and todos.First(t => !t.Completed).Title (or FirstOrDefault to be safe).
β Solution
using System.Net.Http.Json;
record Todo(int Id, int UserId, string Title, bool Completed);
HttpClient client = new HttpClient();
string url = "https://jsonplaceholder.typicode.com/todos";
try
{
List<Todo>? todos = await client.GetFromJsonAsync<List<Todo>>(url);
if (todos is null || todos.Count == 0)
{
Console.WriteLine("No todos returned.");
return;
}
Console.WriteLine($"Total todos: {todos.Count}");
Console.WriteLine($"Completed: {todos.Count(t => t.Completed)}");
Todo? firstOpen = todos.FirstOrDefault(t => !t.Completed);
Console.WriteLine($"First incomplete: {firstOpen?.Title ?? "(none)"}");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Could not reach the API: {ex.Message}");
}
Sample output:
Total todos: 200
Completed: 90
First incomplete: delectus aut autem
π― Quick Quiz
Question 1: What does GetFromJsonAsync<T> do in one call?
Question 2: An HTTP status code in the 4xx range meansβ¦
Question 3: Why reuse a single HttpClient instead of creating one per request?
Summary
π Key Takeaways
- A web API serves data over HTTP at URLs (endpoints), usually as JSON; call it with
HttpClient(all methods are async). GetStringAsyncreturns raw text;GetFromJsonAsync<T>fetches and deserializes into your type in one step.- Check status codes (2xx success, 4xx client error, 5xx server error); use
GetAsync+IsSuccessStatusCodefor control. - Always handle failures with
try/catch (HttpRequestException)β the network can always fail. - Reuse a single
HttpClient(don't create one per request) to avoid socket exhaustion.
π Additional Resources
- Make HTTP requests with HttpClient β Microsoft Docs
- Deserialize JSON (incl. from HttpClient)
- JSONPlaceholder β free fake API for testing
π What's Next?
Your code now talks to the wider world. Before the capstone, we'll make sure it stays correct as it grows. In Lesson 5.2: Unit Testing, you'll write automated tests that prove your code works β and keep it working.
π You're online!
Your programs can fetch live data from anywhere on the internet. Next: proving your code is correct with tests.