HttpClient is the standard way to make HTTP requests to REST APIs in C#. This guide covers everything from basic setup to advanced features and best practices.
Required Namespaces
First, include the necessary namespaces:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
Best Practice: Use HttpClientFactory (Recommended)
In modern .NET applications, use IHttpClientFactory
instead of creating HttpClient instances directly:
// In Startup.cs or Program.cs
services.AddHttpClient();
// In your service or controller
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
}
Basic HttpClient Setup
For simple scenarios, you can create an HttpClient instance:
using var client = new HttpClient();
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
Authentication Headers
Add authentication headers for secured APIs:
// Bearer token
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "your-token-here");
// API Key
client.DefaultRequestHeaders.Add("X-API-Key", "your-api-key");
// Basic authentication
var credentials = Convert.ToBase64String(
Encoding.ASCII.GetBytes("username:password"));
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", credentials);
GET Requests
Simple GET Request
public async Task<string> GetDataAsync()
{
try
{
HttpResponseMessage response = await client.GetAsync("api/users");
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
return content;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Request error: {ex.Message}");
throw;
}
}
GET with Query Parameters
public async Task<string> GetUsersAsync(int page, int limit)
{
string endpoint = $"api/users?page={page}&limit={limit}";
HttpResponseMessage response = await client.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
Deserialize JSON Response
public async Task<List<User>> GetUsersAsync()
{
HttpResponseMessage response = await client.GetAsync("api/users");
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
var users = JsonSerializer.Deserialize<List<User>>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return users;
}
POST Requests
POST with JSON Data
public async Task<User> CreateUserAsync(User newUser)
{
string json = JsonSerializer.Serialize(newUser);
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync("api/users", content);
response.EnsureSuccessStatusCode();
string responseJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<User>(responseJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
POST with Form Data
public async Task<string> PostFormDataAsync()
{
var formData = new List<KeyValuePair<string, string>>
{
new("name", "John Doe"),
new("email", "john@example.com"),
new("age", "30")
};
using var content = new FormUrlEncodedContent(formData);
HttpResponseMessage response = await client.PostAsync("api/users", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
PUT and DELETE Requests
PUT Request (Update)
public async Task<User> UpdateUserAsync(int userId, User updatedUser)
{
string json = JsonSerializer.Serialize(updatedUser);
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PutAsync($"api/users/{userId}", content);
response.EnsureSuccessStatusCode();
string responseJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<User>(responseJson);
}
DELETE Request
public async Task<bool> DeleteUserAsync(int userId)
{
HttpResponseMessage response = await client.DeleteAsync($"api/users/{userId}");
return response.IsSuccessStatusCode;
}
Error Handling and Status Codes
public async Task<ApiResponse<T>> MakeRequestAsync<T>(string endpoint)
{
try
{
HttpResponseMessage response = await client.GetAsync(endpoint);
if (response.IsSuccessStatusCode)
{
string content = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<T>(content);
return new ApiResponse<T> { Success = true, Data = data };
}
else
{
return new ApiResponse<T>
{
Success = false,
ErrorMessage = $"HTTP {response.StatusCode}: {response.ReasonPhrase}"
};
}
}
catch (HttpRequestException ex)
{
return new ApiResponse<T>
{
Success = false,
ErrorMessage = $"Request failed: {ex.Message}"
};
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
return new ApiResponse<T>
{
Success = false,
ErrorMessage = "Request timeout"
};
}
}
Complete Example with Best Practices
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class RestApiClient
{
private readonly HttpClient _httpClient;
public RestApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
_httpClient.DefaultRequestHeaders.Accept.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.Timeout = TimeSpan.FromSeconds(30);
}
public async Task<Post[]> GetPostsAsync()
{
try
{
HttpResponseMessage response = await _httpClient.GetAsync("posts");
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<Post[]>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP Request failed: {ex.Message}");
throw;
}
catch (JsonException ex)
{
Console.WriteLine($"JSON deserialization failed: {ex.Message}");
throw;
}
}
public async Task<Post> CreatePostAsync(Post newPost)
{
try
{
string json = JsonSerializer.Serialize(newPost);
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await _httpClient.PostAsync("posts", content);
response.EnsureSuccessStatusCode();
string responseJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<Post>(responseJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (Exception ex)
{
Console.WriteLine($"Create post failed: {ex.Message}");
throw;
}
}
}
public class Post
{
public int Id { get; set; }
public int UserId { get; set; }
public string Title { get; set; }
public string Body { get; set; }
}
// Usage example
class Program
{
static async Task Main(string[] args)
{
using var httpClient = new HttpClient();
var apiClient = new RestApiClient(httpClient);
// Get all posts
Post[] posts = await apiClient.GetPostsAsync();
Console.WriteLine($"Retrieved {posts.Length} posts");
// Create a new post
var newPost = new Post
{
UserId = 1,
Title = "My New Post",
Body = "This is the content of my new post."
};
Post createdPost = await apiClient.CreatePostAsync(newPost);
Console.WriteLine($"Created post with ID: {createdPost.Id}");
}
}
Key Best Practices
- Use HttpClientFactory in production applications
- Reuse HttpClient instances - don't create new instances for each request
- Set appropriate timeouts to prevent hanging requests
- Handle exceptions properly with try-catch blocks
- Use async/await for better performance and scalability
- Dispose resources with
using
statements when creating HttpClient directly - Validate SSL certificates in production (enabled by default)
- Use strongly-typed models for JSON serialization/deserialization