Yesterday you learned how to turn C# objects into JSON and back again with serialization — turning your beautiful classes into strings that travel across the wire. But travel where exactly? Today we're answering that question. We're talking about HTTP and REST APIs — how your C# code talks to the internet, fetches data from strangers, and sends stuff back. Think of it as teaching your console app to make phone calls.
1. What Even Is a REST API?
Imagine a restaurant. You don't barge into the kitchen and start cooking — you look at the menu, pick an item, and place an order. The kitchen does its thing, and a waiter brings back your food.
A REST API works the same way. The URL is the menu item (/api/posts, /api/users/42), and the HTTP method is how you order:
| HTTP Method | What It Does | Restaurant Analogy |
|---|---|---|
GET |
Fetch data | "Can I see the menu?" |
POST |
Send/create data | "I'd like to order this." |
PUT |
Update existing data | "Actually, change my order." |
DELETE |
Remove data | "Cancel that order." |
REST stands for Representational State Transfer, which sounds like a philosophy lecture but really just means: use URLs to identify things, use HTTP methods to act on them, and send JSON back and forth. You already know JSON from yesterday — so you're halfway there.
2. HttpClient — Your Gateway to the Web
In C#, the class that makes HTTP requests is HttpClient. It lives in System.Net.Http and it's your app's browser — minus the tabs you keep forgetting to close.
Here's the simplest possible GET request:
using System.Net.Http;
HttpClient client = new();
string result = await client.GetStringAsync("https://jsonplaceholder.typicode.com/posts/1");
Console.WriteLine(result);
That's it. Three lines and you just fetched data from the internet. GetStringAsync sends a GET request and returns the response body as a raw string. Since it's async, you await it — just like you learned on Day 18.
JSONPlaceholder is a free fake REST API for testing. It returns dummy data that looks real enough — posts, comments, users — without requiring authentication or sign-ups. It's a playground, and we'll use it throughout this lesson.
3. GET Requests — Fetching Data
You've got three ways to GET data, each progressively more useful:
Option 1: GetStringAsync — returns raw text.
string json = await client.GetStringAsync("https://jsonplaceholder.typicode.com/posts/1");
Console.WriteLine(json);
Good for debugging. Bad for actually using the data, because now you've got a string and you need to deserialize it yourself.
Option 2: GetAsync — returns an HttpResponseMessage with full control.
HttpResponseMessage response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
Console.WriteLine(response.StatusCode); // OK
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
This gives you the status code, headers, and the body. More work, more control.
Option 3: GetFromJsonAsync<T> — the chef's kiss.
using System.Net.Http.Json;
var post = await client.GetFromJsonAsync<Post>("https://jsonplaceholder.typicode.com/posts/1");
Console.WriteLine($"{post.Id}: {post.Title}");
record Post(int UserId, int Id, string Title, string Body);
This one combines the HTTP call and JSON deserialization into a single method. It uses System.Text.Json under the hood — the same serializer you used yesterday. You pass a type parameter <Post>, and it hands you back a fully hydrated C# object. No manual parsing, no intermediate strings.
For most real-world code, GetFromJsonAsync<T> is what you'll reach for.
4. POST Requests — Sending Data
GET is for reading. POST is for sending data to the server — creating a new resource, submitting a form, that sort of thing.
The easy way: PostAsJsonAsync
using System.Net.Http.Json;
var newPost = new
{
title = "My New Post",
body = "This is the content.",
userId = 1
};
HttpResponseMessage response = await client.PostAsJsonAsync(
"https://jsonplaceholder.typicode.com/posts", newPost);
Console.WriteLine(response.StatusCode); // Created
string result = await response.Content.ReadAsStringAsync();
Console.WriteLine(result);
PostAsJsonAsync serializes your object to JSON, sets the Content-Type header to application/json, and sends it. One method, zero hassle.
The manual way: StringContent
Sometimes you need more control — maybe you're sending XML, or form data, or you want to set custom headers on the content:
using System.Text;
using System.Text.Json;
var payload = new { title = "Manual Post", body = "Doing it the hard way.", userId = 1 };
string json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync(
"https://jsonplaceholder.typicode.com/posts", content);
Same result, more typing. Use PostAsJsonAsync unless you have a reason not to.
5. PUT and DELETE — Updating and Removing
PUT replaces an existing resource:
var updatedPost = new { id = 1, title = "Updated Title", body = "New body text.", userId = 1 };
HttpResponseMessage response = await client.PutAsJsonAsync(
"https://jsonplaceholder.typicode.com/posts/1", updatedPost);
Console.WriteLine(response.StatusCode); // OK
Notice the URL includes /posts/1 — you're telling the API which resource to update.
DELETE removes a resource:
HttpResponseMessage response = await client.DeleteAsync(
"https://jsonplaceholder.typicode.com/posts/1");
Console.WriteLine(response.StatusCode); // OK
DELETE doesn't need a body — just the URL. You're saying "get rid of this" and the server complies (or returns an error if it can't).
6. Reading Responses Like a Pro
Every HTTP call returns an HttpResponseMessage. Here's what you can pull from it:
HttpResponseMessage response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
// Status code — was it successful?
Console.WriteLine(response.StatusCode); // OK
Console.WriteLine((int)response.StatusCode); // 200
// Is it a success status code? (200-299)
Console.WriteLine(response.IsSuccessStatusCode); // True
// Read the body
string body = await response.Content.ReadAsStringAsync();
// Or deserialize directly
var post = await response.Content.ReadFromJsonAsync<Post>();
EnsureSuccessStatusCode() is the "throw if it failed" shortcut:
HttpResponseMessage response = await client.GetAsync("https://example.com/api/oops");
response.EnsureSuccessStatusCode(); // Throws HttpRequestException if status is 4xx or 5xx
This is handy when you don't want to manually check — if the server returns a 404 or 500, the method throws an HttpRequestException and your try/catch block from Day 9 kicks in.
Here are the status codes you'll see most often:
| Code | Name | Meaning |
|---|---|---|
| 200 | OK | Everything worked |
| 201 | Created | New resource was created |
| 204 | No Content | Success, but nothing to return |
| 400 | Bad Request | You sent garbage |
| 401 | Unauthorized | Who are you? |
| 403 | Forbidden | I know who you are, but no |
| 404 | Not Found | That URL doesn't exist |
| 500 | Internal Server Error | The server broke — not your fault |
7. Headers, Query Strings, and Error Handling
Custom headers — some APIs require an API key or accept custom headers:
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("X-Api-Key", "your-key-here");
Be careful with DefaultRequestHeaders — they apply to every request made by that HttpClient instance. If you need per-request headers, create an HttpRequestMessage manually:
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data");
request.Headers.Add("Authorization", "Bearer my-token");
HttpResponseMessage response = await client.SendAsync(request);
Query strings — don't concatenate URLs by hand like it's 2005:
// Bad — fragile and easy to mess up
string url = "https://api.example.com/posts?userId=" + userId + "&page=" + page;
// Better — let the framework handle encoding
var query = System.Web.HttpUtility.ParseQueryString(string.Empty);
query["userId"] = userId.ToString();
query["page"] = page.ToString();
string url = $"https://api.example.com/posts?{query}";
Error handling — wrap your HTTP calls in try/catch:
try
{
var posts = await client.GetFromJsonAsync<List<Post>>(
"https://jsonplaceholder.typicode.com/posts");
Console.WriteLine($"Got {posts?.Count} posts");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP error: {ex.Message}");
}
catch (TaskCanceledException)
{
Console.WriteLine("Request timed out.");
}
HttpRequestException covers network failures and bad status codes (if you called EnsureSuccessStatusCode). TaskCanceledException fires when a request times out — yes, it's a confusing name, but that's .NET for you.
8. IHttpClientFactory — Why You Shouldn't new Up HttpClient
Here's a trap that catches almost everyone. This looks perfectly innocent:
// DON'T DO THIS in real applications
for (int i = 0; i < 100; i++)
{
using var client = new HttpClient();
var result = await client.GetStringAsync("https://example.com");
}
Each new HttpClient() opens a new socket connection. Even after you Dispose it, the underlying socket lingers in a TIME_WAIT state for up to 240 seconds. Do this enough times and you'll exhaust the available sockets on your machine — a problem called socket exhaustion. Your app starts throwing SocketException errors, other apps on the same machine can't connect to anything, and you're having a very bad day.
The fix: reuse a single HttpClient or use IHttpClientFactory.
For console apps, a single static instance works fine:
// This is safe — reuse one instance
static readonly HttpClient client = new();
For real applications (ASP.NET Core, worker services), use IHttpClientFactory:
// In Program.cs — register the factory
builder.Services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
});
// In your service — inject and create clients
public class GitHubService(IHttpClientFactory httpClientFactory)
{
public async Task<string> GetReposAsync()
{
HttpClient client = httpClientFactory.CreateClient("GitHub");
return await client.GetStringAsync("repos");
}
}
IHttpClientFactory manages the connection pool for you — it reuses handlers, rotates them to respect DNS changes, and prevents socket exhaustion. It's the recommended approach for any application that lives longer than a quick script.
Named clients (like "GitHub" above) let you configure different base URLs, headers, and timeouts for different APIs — all registered in one place.
Your Homework: Build a JSONPlaceholder Console App
Build a console app that fetches all posts from JSONPlaceholder, then displays them in a formatted list. Here's your starting point:
using System.Net.Http.Json;
HttpClient client = new();
var posts = await client.GetFromJsonAsync<List<Post>>(
"https://jsonplaceholder.typicode.com/posts");
if (posts is null)
{
Console.WriteLine("No posts returned.");
return;
}
Console.WriteLine($"=== JSONPlaceholder Posts ({posts.Count} total) ===\n");
foreach (var post in posts.Take(10)) // Show first 10
{
Console.WriteLine($"#{post.Id} [{post.UserId}] {post.Title}");
Console.WriteLine($" {post.Body[..Math.Min(80, post.Body.Length)]}...");
Console.WriteLine();
}
record Post(int UserId, int Id, string Title, string Body);
Stretch goals:
- Add a
GETrequest to fetch comments for a specific post (/posts/1/comments) and display the commenter's email and body. - Add a
POSTrequest to create a new post, then print the response. - Wrap everything in
try/catchand handle bothHttpRequestExceptionandTaskCanceledException. - Add a menu: let the user choose between listing posts, viewing a single post, or creating a new one.
Summary of Day 26
- A REST API uses URLs to identify resources and HTTP methods (
GET,POST,PUT,DELETE) to act on them. HttpClientis C#'s built-in class for making HTTP requests — it lives inSystem.Net.Http.GetFromJsonAsync<T>andPostAsJsonAsynccombine HTTP calls with JSON serialization, saving you from manual parsing.- Every response carries a status code — check
IsSuccessStatusCodeor callEnsureSuccessStatusCode()to handle failures. - Use
HttpRequestMessagewhen you need per-request headers or full control over the request. - Never create a new
HttpClientper request in long-running apps — you'll hit socket exhaustion. Use a single shared instance orIHttpClientFactory. IHttpClientFactorywith named clients lets you configure different APIs cleanly and manage connection lifetimes automatically.
Tomorrow: we'll explore Collections Deep Dive — Stack, Queue, HashSet, and picking the right collection for the job. Because List<T> is great, but it's not the only box on the shelf. 📦
See you on Day 27!