This guide explains how to make API calls to other services from your microservices. There are two main approaches depending on the type of service you're calling.
- ✅ Calling external third-party APIs (GitHub, Azure, Stripe, weather APIs, etc.)
- ✅ The service uses REST/HTTP protocol
- ✅ The service is not built using the cloops.microservices framework
- ✅ You need standard HTTP features (headers, query parameters, different HTTP methods)
- ✅ Calling other microservices built with cloops.microservices
- ✅ You need type-safe communication with compile-time checking
- ✅ You want automatic message validation
- ✅ You need request-reply, event publishing, or stream publishing patterns
- ✅ You want to leverage NATS features (distributed messaging, JetStream, etc.)
Is the service built with cloops.microservices?
├─ YES → Use NATS Subject Builders (R_Subject, P_Subject, S_Subject)
└─ NO → Use HTTP Service
HTTP services are singleton-friendly wrappers around IHttpClientFactory. Use them for outbound calls to third-party HTTP APIs. The framework registers HTTP services automatically, and the recommended service implementation creates a fresh logical HttpClient for each outbound operation through the factory.
This follows Microsoft's IHttpClientFactory guidance: HTTP requests with IHttpClientFactory - basic usage.
NATS consumers/controllers are singletons. Do not store one injected
HttpClientin a controller or HTTP service and reuse it forever. StoreIHttpClientFactoryinstead, or inherit fromBaseHttpService, and create a client inside each method call.
To create an HTTP service:
- Create a class in a namespace ending with
Services.Http - Inherit from
BaseHttpServiceor injectIHttpClientFactory - Create a client inside each outbound method call
- Customize the client by overriding
ConfigureClientorCreateClient
Example:
using System.Net.Http;
using CLOOPS.microservices;
using Microsoft.Extensions.Logging;
namespace myapp.services.http;
public class WeatherService : BaseHttpService
{
private readonly ILogger<WeatherService> _logger;
public WeatherService(
IHttpClientFactory httpClientFactory,
ILogger<WeatherService> logger) : base(httpClientFactory, logger)
{
_logger = logger;
}
protected override void ConfigureClient(HttpClient client)
{
client.BaseAddress = new Uri("https://api.weather.com/");
client.DefaultRequestHeaders.Add("X-API-Key", "your-api-key");
client.Timeout = TimeSpan.FromSeconds(30);
}
}Here's a complete example of making an HTTP GET request:
namespace myapp.services.http;
public class WeatherService : BaseHttpService
{
private readonly ILogger<WeatherService> _logger;
public WeatherService(
IHttpClientFactory httpClientFactory,
ILogger<WeatherService> logger) : base(httpClientFactory, logger)
{
_logger = logger;
}
protected override void ConfigureClient(HttpClient client)
{
client.BaseAddress = new Uri("https://api.weather.com/v1/");
}
public async Task<WeatherData?> GetWeatherAsync(string city, CancellationToken ct = default)
{
try
{
using var httpClient = CreateClient();
using var response = await httpClient.GetAsync($"weather?city={city}", ct);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync(ct);
var weatherData = Util.Deserialize<WeatherData>(json);
return weatherData;
}
else
{
_logger.LogWarning("Weather API returned status {StatusCode}", response.StatusCode);
return null;
}
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to get weather for city {City}", city);
return null;
}
}
}
public class WeatherData
{
public string City { get; set; } = "";
public double Temperature { get; set; }
public string Condition { get; set; } = "";
}For POST requests with JSON payload:
public async Task<bool> CreateUserAsync(User user, CancellationToken ct = default)
{
try
{
using var httpClient = CreateClient();
var json = Util.Serialize(user);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
using var response = await httpClient.PostAsync("users", content, ct);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create user");
return false;
}
}Always check the response status and handle different scenarios:
public async Task<ApiResult<T>> GetDataAsync<T>(string endpoint, CancellationToken ct = default)
{
try
{
using var httpClient = CreateClient();
using var response = await httpClient.GetAsync(endpoint, ct);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync(ct);
var data = Util.Deserialize<T>(json);
return new ApiResult<T> { Success = true, Data = data };
}
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return new ApiResult<T> { Success = false, Error = "Resource not found" };
}
else
{
var errorContent = await response.Content.ReadAsStringAsync(ct);
return new ApiResult<T>
{
Success = false,
Error = $"API returned {response.StatusCode}: {errorContent}"
};
}
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
return new ApiResult<T> { Success = false, Error = "Request was cancelled" };
}
catch (Exception ex)
{
_logger.LogError(ex, "HTTP request failed");
return new ApiResult<T> { Success = false, Error = ex.Message };
}
}
public class ApiResult<T>
{
public bool Success { get; set; }
public T? Data { get; set; }
public string? Error { get; set; }
}Always wrap HTTP calls in try-catch blocks to handle:
- Network failures (DNS resolution, connection timeouts)
- HTTP errors (4xx, 5xx status codes)
- Cancellation requests
public async Task<string?> FetchDataAsync(string url, CancellationToken ct = default)
{
try
{
using var httpClient = CreateClient();
using var response = await httpClient.GetAsync(url, ct);
response.EnsureSuccessStatusCode(); // Throws if status code is not success
return await response.Content.ReadAsStringAsync(ct);
}
catch (HttpRequestException ex)
{
// Handle HTTP-specific errors
_logger.LogError(ex, "HTTP error when calling {Url}", url);
return null;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
// Handle cancellation
_logger.LogInformation("Request to {Url} was cancelled", url);
return null;
}
catch (Exception ex)
{
// Handle other errors (network failures, etc.)
_logger.LogError(ex, "Unexpected error when calling {Url}", url);
return null;
}
}When calling other microservices built with cloops.microservices, use Subject Builders and strongly typed messages. This provides compile-time type safety and automatic validation.
Subject builders provide type-safe access to NATS subjects. They ensure:
- ✅ Type safety: You can only publish the correct message type to each subject
- ✅ Compile-time checking: Errors are caught before runtime
- ✅ IntelliSense support: Autocomplete for subjects and messages
- ✅ Automatic validation: Messages are validated before sending
Use R_Subject when you need to send a request and wait for a response (like a synchronous API call).
Example: Getting jobs from CLJPS service
using CLOOPS.NATS;
using CLOOPS.NATS.Messages.CLJPS;
using CLOOPS.NATS.Extensions;
namespace myapp.services;
public class JobQueryService
{
private readonly ICloopsNatsClient _client;
private readonly ILogger<JobQueryService> _logger;
public JobQueryService(ICloopsNatsClient client, ILogger<JobQueryService> logger)
{
_client = client;
_logger = logger;
}
public async Task<List<Job>?> GetJobsAsync(JobStatus status, CancellationToken ct = default)
{
try
{
// 1. Create a request message
var request = new GetJobsRequest
{
jobStatus = status,
limit = 50,
offset = 0
};
// 2. Get the subject using subject builder
// Assuming there's a subject builder method: R_GetJobs()
var subject = _client.Subjects().CLJPS().R_GetJobs();
// 3. Send request and wait for response
var response = await subject.Request(request, ct);
// 4. Handle the response
if (response?.Data != null)
{
return response.Data;
}
return null;
}
catch (ValidationException ex)
{
// Message validation failed before sending
_logger.LogError(ex, "Request validation failed");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get jobs");
return null;
}
}
}Key Points:
R_Subject<RequestType, ResponseType>is strongly typed- The
Request()method validates the message before sending - You get a typed response back
- If validation fails, a
ValidationExceptionis thrown before the request is sent
Use P_Subject for fire-and-forget event publishing (Core NATS).
Example: Publishing a person save event
using CLOOPS.NATS;
using CLOOPS.NATS.Extensions;
namespace myapp.services;
public class PersonService
{
private readonly ICloopsNatsClient _client;
public PersonService(ICloopsNatsClient client)
{
_client = client;
}
public async Task PublishPersonSavedAsync(Person person, CancellationToken ct = default)
{
try
{
// 1. Create the message
var personMessage = new Person
{
Id = person.Id,
Name = person.Name,
Age = person.Age,
Addr = person.Addr
};
// 2. Get the subject using subject builder
var subject = _client.Subjects().Example().P_SavePerson(person.Id);
// 3. Publish (fire-and-forget)
await subject.Publish(personMessage, ct);
// Message is validated automatically before publishing
}
catch (ValidationException ex)
{
// Validation failed - message was not published
throw new InvalidOperationException("Person data is invalid", ex);
}
}
}Key Points:
P_Subject<T>is for Core NATS publishingPublish()validates the message before sending- This is fire-and-forget (no response expected)
Use S_Subject for JetStream publishing (durable, persistent events).
Example: Publishing a job update event to JetStream
using CLOOPS.NATS;
using CLOOPS.NATS.Extensions;
namespace myapp.services;
public class JobService
{
private readonly ICloopsNatsClient _client;
public JobService(ICloopsNatsClient client)
{
_client = client;
}
public async Task ScheduleJobAsync(Job job, CancellationToken ct = default)
{
try
{
// 1. Create the job message
var jobMessage = new Job
{
Id = job.Id,
JobUrl = job.JobUrl,
JobHttpMethod = job.JobHttpMethod,
ExpectedExecutionAt = job.ExpectedExecutionAt
};
// 2. Get the subject using subject builder
var subject = _client.Subjects().CLJPS().S_ScheduleJob(job.Id);
// 3. Publish to JetStream (durable)
// Parameters: message, waitForAck, messageId
await subject.StreamPublish(jobMessage, waitForAck: true, messageId: job.Id, ct: ct);
// Message is validated and persisted in JetStream
}
catch (ValidationException ex)
{
// Validation failed - message was not published
throw new InvalidOperationException("Job data is invalid", ex);
}
}
}Key Points:
S_Subject<T>is for JetStream publishingStreamPublish()creates durable, persistent events- Use
waitForAck: trueto ensure the message is persisted - Messages are validated before publishing
Here's a service that uses both HTTP calls and NATS subject builders:
using CLOOPS.NATS;
using CLOOPS.NATS.Extensions;
using CLOOPS.NATS.Messages.CLJPS;
namespace myapp.services;
public class OrderProcessingService
{
private readonly ICloopsNatsClient _client;
private readonly PaymentService _paymentService; // HTTP service
private readonly ILogger<OrderProcessingService> _logger;
public OrderProcessingService(
ICloopsNatsClient client,
PaymentService paymentService,
ILogger<OrderProcessingService> logger)
{
_client = client;
_paymentService = paymentService;
_logger = logger;
}
public async Task<bool> ProcessOrderAsync(Order order, CancellationToken ct = default)
{
try
{
// 1. Call external payment API (HTTP)
var paymentResult = await _paymentService.ProcessPaymentAsync(
order.PaymentDetails, ct);
if (!paymentResult.Success)
{
_logger.LogWarning("Payment failed for order {OrderId}", order.Id);
return false;
}
// 2. Publish order created event to other microservices (NATS)
var orderEvent = new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Amount = order.Amount,
Timestamp = DateTimeOffset.UtcNow
};
var subject = _client.Subjects().Orders().S_OrderCreated(order.Id);
await subject.StreamPublish(orderEvent, waitForAck: true, messageId: order.Id, ct: ct);
// 3. Request inventory update from inventory service (NATS)
var inventoryRequest = new UpdateInventoryRequest
{
OrderId = order.Id,
Items = order.Items
};
var inventorySubject = _client.Subjects().Inventory().R_UpdateInventory();
var inventoryResponse = await inventorySubject.Request(inventoryRequest, ct);
if (inventoryResponse?.Data?.Success == true)
{
_logger.LogInformation("Order {OrderId} processed successfully", order.Id);
return true;
}
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
return false;
}
}
}-
One HTTP service per external API service: Create separate services for different third-party API applications (e.g.,
GitHubService,AzureService,StripeService) -
Create clients per outbound call: NATS consumers are singleton, so create a client from
IHttpClientFactoryorBaseHttpService.CreateClient()inside each method call -
Customize clients in one place: Override
ConfigureClientfor base URLs, headers, and timeouts, or overrideCreateClientfor advanced behavior -
Always handle errors: Wrap HTTP calls in try-catch blocks to handle network failures
-
Use cancellation tokens: Always pass
CancellationTokento async HTTP methods -
Don't create HttpClient manually with
new: Always useIHttpClientFactorydirectly or throughBaseHttpService
-
Always use subject builders: Never construct subjects manually; use builders for type safety
-
Trust the validation: Messages are validated automatically, so handlers can assume valid data
-
Handle ValidationException: Catch validation exceptions when publishing/requesting
-
Choose the right subject type:
R_Subject: When you need a response (request-reply)P_Subject: For fire-and-forget events (Core NATS)S_Subject: For durable, persistent events (JetStream)
-
Use meaningful message IDs: When using
StreamPublish, provide meaningful message IDs for deduplication -
Wait for acknowledgments: Use
waitForAck: truefor critical events that must be persisted
-
Log important operations: Log successes and failures for debugging
-
Use structured logging: Include relevant context (IDs, status codes, etc.) in log messages
-
Handle cancellation: Always respect
CancellationTokenand handleOperationCanceledException -
Validate inputs: Even though NATS messages are validated automatically, validate inputs for HTTP calls
- HTTP Services: Use for external third-party APIs (GitHub, Azure, payment gateways, etc.)
- NATS Subject Builders: Use for calling other cloops.microservices with type safety and validation
- Subject Types:
R_Subject: Request-reply (synchronous calls)P_Subject: Fire-and-forget events (Core NATS)S_Subject: Durable events (JetStream)
- Always handle errors: Both HTTP and NATS calls can fail
- Use cancellation tokens: Respect cancellation requests in async operations
By following these patterns, you'll build reliable, type-safe communication between your microservices and external APIs.