Day 26: HTTP and REST APIs - Teaching Your Console App to Talk to the Internet
arrow_back All Posts
April 06, 2026 9 min read .NET/C#

Day 26: HTTP and REST APIs - Teaching Your Console App to Talk to the Internet

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 GET request to fetch comments for a specific post (/posts/1/comments) and display the commenter's email and body.
  • Add a POST request to create a new post, then print the response.
  • Wrap everything in try/catch and handle both HttpRequestException and TaskCanceledException.
  • 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.
  • HttpClient is C#'s built-in class for making HTTP requests — it lives in System.Net.Http.
  • GetFromJsonAsync<T> and PostAsJsonAsync combine HTTP calls with JSON serialization, saving you from manual parsing.
  • Every response carries a status code — check IsSuccessStatusCode or call EnsureSuccessStatusCode() to handle failures.
  • Use HttpRequestMessage when you need per-request headers or full control over the request.
  • Never create a new HttpClient per request in long-running apps — you'll hit socket exhaustion. Use a single shared instance or IHttpClientFactory.
  • IHttpClientFactory with 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!

Share
FM

Farhad Mammadov

.NET Engineer & Cloud Architect · Bayern, Germany. Writing about scalable backend systems, AWS, and SRE.