ai newsDo., 29. Okt.
EilmeldungClaude Code for beginners: build your first app without touching a terminal

Your AI Agent Is Only as Good as the API It Calls

Your AI agent is only as good as the API it calls. I wired an agent to a NestJS backend last quarter. It had to create orders, check stock, and issue refunds. The agent failed constantly. Not because the model was dumb. Because the endpoints were built for a frontend, not for a machine: five calls

dev.toDonnerstag 06:48 · 29.10.8 Min. Lesezeit
Bild: dev.to

Your AI agent is only as good as the API it calls.

I wired an agent to a NestJS backend last quarter. It had to create orders, check stock, and issue refunds. The agent failed constantly. Not because the model was dumb. Because the endpoints were built for a frontend, not for a machine: five calls to do one job, huge payloads, vague errors.

That mismatch is easy to miss, because the API works fine when a human drives it through a user interface. A human sees a screen, fills in what is missing, and recovers from a vague error without thinking. An agent cannot do any of that. It reads what you return, decides what to do next from that text alone, and acts on it. Every gap in your API becomes a decision the model has to guess at, and guesses are where bad database writes come from.

The first wrong approach is to expose your REST endpoints directly as agent tools. A frontend endpoint is designed to serve a screen. It returns everything that screen might need, paginates in a way that suits scrolling, and assumes the caller already knows the flow. Hand that to an agent and it will fetch far more data than it needs, miss the one field that matters, and chain calls in the wrong order, because nothing in the response told it the right order.

The second wrong approach is worse: build one giant do everything endpoint and let the model figure it out. That looks flexible. In practice it gives the agent too much room to improvise, and improvisation is where hallucinations turn into real database writes. A tool that can do anything cannot be tested properly, cannot be logged properly, and cannot be reasoned about when something goes wrong. When a refund is issued by mistake, you want to know exactly which tool did it and exactly what it received. A giant endpoint cannot tell you that.

I keep a thin tool layer on top of the real services.

Each tool does exactly one thing, takes typed inputs, and returns a small predictable shape. The business logic stays in the service. The tool is just a translator between the agent and the service, nothing more.

// tools/order.tools.ts import { OrdersService } from "./orders.service "; export interface OrderItem { productId: string; quantity: number; } export class OrderTools { constructor ( private readonly orders: OrdersService ) {} @ Tool ({ name: " create_order ", description: " Create an order for a customer " }) async createOrder ( @ Param ( " customerId " ) customerId: string, @ Param ( " items " ) items: OrderItem [], ) { const order = await this. orders. create ({ customerId, items }); return { orderId: order. id, status: order. status, total: order. total, }; } @ Tool ({ name: " check_stock ", description: " Check stock for one product " }) async checkStock (@ Param ( " productId " ) productId: string ) { const stock = await this. orders. checkStock ( productId ); return { productId, inStock: stock. quantity > 0, quantity: stock. quantity }; } @ Tool ({ name: " issue_refund ", description: " Issue a refund for a paid order " }) async issueRefund ( @ Param ( " orderId " ) orderId: string, @ Param ( " reason " ) reason: string, ) { const refund = await this. orders. refund ( orderId, reason ); return { orderId, refundId: refund. id, status: refund. status }; } } Enter fullscreen mode Exit fullscreen mode Three things matter in that code, and none of them is the decorator syntax.

First, the service owns the logic. Creating an order, checking stock, issuing a refund: all the validation, all the rules about what can be refunded and when, all of it lives in OrdersService, exactly where it lived before the agent arrived. The tool adds no rules of its own. If the service changes, the tool still works, because the tool only passes inputs through and shapes the output.

Second, the return shape is small and stable. The agent does not get the full order object with every relation attached. It gets an id, a status, and a total. That is enough for it to continue the conversation and to tell the user what happened. Small outputs also keep the model focused: a huge payload is not just wasteful, it is a distraction that makes the next decision worse.

Third, the agent never touches the service directly. It only sees small honest tools. That boundary is what makes the rest of the system manageable.

An agent treats an error message as information, so a vague error is worse than no error at all. "Something went wrong" tells the model nothing, so it retries, or tries a different tool, or invents a workaround. A structured error tells it what actually happened and what it can do about it.

Return errors in the same shape every time: a short code the agent can match on, and a plain message a human can read. Keep the codes small in number. A handful of codes like not found, out of stock, already refunded, and invalid input covers almost everything an order flow needs. If the agent sees "already refunded", it stops instead of refunding twice. If it sees "out of stock", it can tell the user honestly instead of placing an order that will fail later.

Never leak internals in an error. Stack traces, database messages, and internal service names are not information for the model, they are noise that it will sometimes repeat to the user. Log those on the server, where they belong, and return only the code and the message.

Every tool call is logged like any other API call: which tool ran, what inputs it received, what it returned, and whether it succeeded. That log is what turns a strange outcome into a debuggable one. When a user says the agent refunded the wrong order, you do not have to guess what the model did. You read the log, you see the tool, the order id, and the reason it was given, and you know exactly where the chain broke.

That log is also your safety net for destructive tools. A tool that only reads data is low risk. A tool that writes data, moves money, or changes state deserves more care. In production I keep write tools behind the same checks any write endpoint would have: authentication, permission checks, and validation in the service. For the most sensitive actions, like issuing a refund, the tool should require an explicit confirmation from the user before it runs, not just a confident model. The model proposes, the human confirms, the tool executes. That one rule removes a whole class of expensive mistakes.

A tool layer is easy to test, and that is one of its best arguments. The service already has its own tests. The tool needs tests of its own, for a different question: does this tool accept what the agent will actually send, and does it return the shape the agent expects?

Write a test that calls the tool with realistic inputs, including messy ones: a missing field, an empty items list, a product id that does not exist. Assert the output shape, not just that it did not throw. If the output shape drifts, the agent's next step drifts with it, so the shape is the contract. Testing it is cheap, and it catches the regressions that are hardest to spot by reading code, like a renamed field that silently becomes undefined in the agent's view.

I would not let the model write queries, call a database directly, or reach an endpoint that was built for an admin screen. Every one of those shortcuts removes the layer that keeps the agent honest. The service layer exists for a reason: it enforces the rules, it guards the data, and it keeps one version of the truth. The moment the agent bypasses it, you have two versions of your business logic, and the untested one is the one making writes.

I would also not return full objects "just in case" the agent needs a field later. If it needs that field, add it to the tool output deliberately, with a name that means something. A tool output is an interface, and interfaces grow by decision, not by accident.

Agents do not need a new backend. They need a backend that speaks in small, honest, predictable steps. One tool, one job, typed inputs, a small output, errors the model can act on, and a log for every call. That is not a lot of code. In the project I described, the tool layer was a fraction of the size of the services underneath it, and it was the part that made the agent reliable.

The model gets the credit when an agent works well, but most of the time the difference between a flaky agent and a dependable one is not the model at all. It is the API behind it, and whether that API was built for a machine that reads text and acts on it, or for a screen that a human was always going to interpret.

Do you expose your APIs to agents directly, or do you keep a separate tool layer?

I'm Sohail Khan, a Senior Full Stack Developer building AI agents and automation. Tech Lead at LoopxCorp. I write about backend engineering, AI integration and practical system design.

Quelle: dev.to (dev.to). Original öffnen