Oct 14, 2025 · 3 min read · Real Engineering Lessons
Building a Reliable API Integration
The endpoint was simple. The real work started when the operation had to handle an unavailable service, repeated requests and a process that could stop after only some steps.
The endpoint was the easy part. The harder part came when we connected a government platform at Softcraft to services outside our control. One of its workflows looked simple from the outside. A user initiated an operation, our API validated the request, and another system completed part of the process. That explanation fits in one sentence, but the real flow had more questions. What happens if the external service is unavailable? What if it completes the operation but our connection ends before we receive the response? What if the user sends the same request again? What if our database update succeeds but the next stage doesn’t? Writing the endpoint was only the beginning.
The Happy Path
On the happy path, the API received valid data, called the external service, recorded the result, and returned a successful response. But external systems don’t share our deployment schedule, latency expectations, or incident timeline. A purely synchronous workflow made their temporary problems immediately become our users’ problems. We changed parts of the flow to use asynchronous processing. The API could validate and accept the work, while workers handled the integration with controlled retries. We recorded the state of the operation so the system knew what had completed and what still needed attention. That change reduced processing time from minutes to seconds in important workflows and made temporary failures easier to recover from. We built the flow with Node.js, TypeScript, NestJS, Redis, and PostgreSQL. To the user, the result was simpler: the workflow became faster and less fragile.
What Does “Try Again” Mean?
Adding retries sounds like an obvious reliability improvement. It’s only safe when the operation can handle repetition. Imagine that the external service completes a request, but the response is lost. For our system, the call failed. Sending it again may be correct, or it may make the business action happen twice. We needed a way to recognize the operation across attempts and avoid treating every retry as new work. We also needed limits. A dependency that’s unavailable for a long period shouldn’t receive an endless stream of increasingly desperate requests. At this point, the HTTP response isn’t the whole story anymore. The system still needs to understand the operation when the communication is incomplete.
Partial States
We also stopped thinking about the flow as only success or failure. A request could be validated but not sent, sent but not confirmed, or confirmed externally while our local update was still pending. We made these stages visible enough for the application and the team to see where processing stopped. Operation identifiers and useful logs came from the same need. During support, “the integration failed” didn’t help much. We needed to know which operation failed, on which attempt and after which completed step. The controller and the HTTP response were only a small part of what the user experienced.
I still like a clean API and I still care about the contract, but this project made the work behind the endpoint much more visible to me. In later reviews I kept returning to the same awkward situations: the response that never arrived, the repeated request and the operation that stopped in the middle. They weren’t the interesting part of a demo, but they were usually the part we talked about when something failed.