When building an API, a common scenario is making outbound API calls within your controller’s logic and handling errors appropriately. One of the more frequent challenges is when the external API returns a 404 (Not Found), and we want to ensure this error is propagated back to the client calling our controller. In this post, I’ll walk through a clean, maintainable way to handle this situation in .NET 8 using modern patterns.
Thank me by sharing on Twitter 🙏
When you’re building an API, maintaining clarity in error handling is key to ensuring a great developer experience for both your team and your users. In this post, I’ll share my approach for handling outbound API errors—particularly 404s—and how to structure your code to propagate them properly.
Breaking Down the Scenario
Imagine you have a .NET 8 controller that needs to fetch data from a third-party API. If that API returns a 404, you want to make sure that your controller also returns a 404 to the client calling your route, instead of generic error responses or exceptions that might confuse the client.
To solve this, we need to:
- Make the outbound call using
HttpClient
. - Handle the potential 404 response from the third-party service.
- Propagate that error back to our controller.
- Return a well-structured 404 response from the controller.
Structuring the Service Layer
I usually start by structuring the service layer, which handles the outbound API calls. Here, I’m using the built-in HttpClient
. It’s important that the service not only makes the call but also interprets the response correctly and throws an appropriate exception when the resource is not found.
The Bitcoin Standard: The Decentralized Alternative to Central Banking
$22.11 (as of December 21, 2024 19:39 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Amazon Basics Micro SDXC Memory Card with Full Size Adapter, A2, U3, Read Speed up to 100 MB/s, 128 GB, Black
$10.99 (as of December 21, 2024 08:38 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)HUANUO Dual Monitor Stand - Full Adjustable Monitor Desk Mount Swivel Vesa Bracket with C Clamp, Grommet Mounting Base for 13 to 32 Inch Computer Screens - Each Arm Holds 4.4 to 19.8lbs
$59.99 (as of December 21, 2024 08:38 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Here’s what this looks like in TypeScript, which mirrors how you’d do it in .NET 8:
class MyApiService {
private httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
async getData(id: string): Promise<MyDataDto> {
const response = await this.httpClient.get(`https://api.example.com/data/${id}`);
if (response.status === 404) {
// Throw a custom error if the resource is not found
throw new ResourceNotFoundException("Data not found");
}
if (!response.ok) {
// Handle other error scenarios
throw new Error("Something went wrong while fetching the data");
}
return response.json();
}
}
// Custom exception class to indicate a 404 error
class ResourceNotFoundException extends Error {
constructor(message: string) {
super(message);
this.name = "ResourceNotFoundException";
}
}
This code sets up a MyApiService
class that performs the API call. Notice how it checks for a 404 response and throws a custom exception, ResourceNotFoundException
. This makes our error handling more expressive and keeps our service logic clean.
Propagating Errors in the Controller
Next, the controller needs to catch the specific error and respond accordingly. This allows the client calling your API to receive a 404 status code when the resource isn’t found, just as if they were interacting directly with the third-party API.
Here’s how I approach this in the controller:
@Controller('api/data')
export class MyController {
constructor(private readonly myApiService: MyApiService) {}
@Get(':id')
async getData(@Param('id') id: string): Promise<MyDataDto> {
try {
const result = await this.myApiService.getData(id);
return result;
} catch (error) {
if (error instanceof ResourceNotFoundException) {
throw new HttpException('Resource not found', HttpStatus.NOT_FOUND);
}
throw new HttpException('Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
In this controller, I’m catching the ResourceNotFoundException
thrown by the service and mapping it to a 404 HTTP status code. For any other unhandled exceptions, I’m returning a 500 status code to indicate a general server error.
By structuring the controller in this way, the error flow is clean: when the service detects a missing resource, it triggers the correct exception, and the controller translates that into a response suitable for the client.
Advantages of This Approach
- Clarity and Separation of Concerns: By handling the 404 logic in the service layer and propagating the error to the controller, we maintain a clean separation of concerns. Each layer does what it’s supposed to do without leaking unnecessary details to other parts of the codebase.
- Custom Exceptions for Better Error Handling: Introducing custom exceptions like
ResourceNotFoundException
keeps our code expressive and easier to maintain. It’s clear what’s happening and why, which benefits other developers reading or maintaining your code. - Consistent API Responses: Propagating the correct status codes ensures that the clients of your API receive consistent and meaningful responses, making the API easier to work with.
Wrapping Up
Handling outbound API calls and propagating errors correctly is crucial for building reliable and intuitive APIs. By leveraging a service layer that handles the outbound API logic and using custom exceptions, you can maintain clean, maintainable code while giving your clients the correct feedback when things go wrong.
The approach I’ve shared here is easy to extend for more complex scenarios, and it scales well as your application grows. Remember, a well-structured error-handling flow not only improves your API’s reliability but also makes life easier for everyone interacting with your endpoints.
This strategy has worked well for me, and I hope you find it useful in your own .NET 8 projects.