Every other client conversation now starts with "can we add AI to this?" The demo is always easy — wire an LLM call to a button, watch it summarize something, everyone claps. Then it goes to production and the questions get real: who pays for the tokens, what happens when the model is slow, and where does this code even live?
After shipping a handful of these on a .NET + Next.js stack, I've settled on a few rules for where the automation layer belongs. Put it in the wrong place and you feel it within a week.
The default: automation lives in .NET, not Next.js
The temptation is to call the LLM straight from a Next.js route handler. It's right there, it's fast to write, and the API key is "hidden" on the server. Don't.
The AI work belongs behind your .NET API for the same reasons your database queries do:
- Secrets and spend stay in one place. One service holds the model keys, enforces rate limits, and tracks token cost per customer. Scattering that across Next.js routes means you find out about a runaway bill from your provider, not your logs.
- Long-running work needs a long-running process. Agentic workflows retry, call tools, and chain steps. That's seconds, sometimes minutes. .NET's long-lived processes and
BackgroundServicehandle that natively; a serverless Next.js function times out and leaves you with half-finished state. - You already have the domain logic there. The automation almost always needs to read and write your real data. Keeping it next to EF Core means no second copy of your business rules.
Next.js stays what it's good at: rendering the result, streaming tokens to the UI, and showing progress.
The boundary: treat the model like an unreliable third-party API
The mistake I see most is treating an LLM call like a function that returns a value. It isn't. It's a network call to a flaky, occasionally-wrong, sometimes-slow external service — closer to a payment gateway than a method.
So I wrap it the way I'd wrap any third-party dependency:
- Timeouts and retries with backoff, in the .NET service. The default HTTP timeout is not your friend here.
- A typed contract on the way out. I make the model return JSON and validate it against a C# record before anything downstream touches it. If it doesn't parse, that's a handled error, not an exception three layers deep.
- A fallback path. When the model is down or the output fails validation, the feature degrades — it doesn't take the request with it.
This is the part the demo never shows, and it's 80% of why "production AI" feels different from "AI in a notebook."
The async problem: don't make the user wait on a token stream
A summarize-this-paragraph call is fine to await. A real agentic workflow that hits three tools and reasons over a document is not — the user is staring at a spinner for 40 seconds, and your request is holding a connection open the whole time.
The pattern that's held up for me:
- The Next.js client kicks off the job and gets back an id immediately.
- .NET runs the workflow in the background, writing status as it goes.
- The UI streams progress — either polling or Server-Sent Events for the token stream.
It's more moving parts than await llm(), but it's the difference between a feature that works for one user in a demo and one that survives real traffic.
Where the no-code tools still earn their place
Not everything deserves custom code. For the glue work — "when a form comes in, enrich it, draft a reply, drop it in the CRM" — I reach for n8n or a similar runner before I write a line of C#.
The rule I use: if it's orchestration between external services, automate it with a workflow tool; if it touches my own data or domain logic, it goes in .NET. The two meet over a webhook. That split has saved me from rebuilding Zapier inside an API more than once.
TL;DR
- Put the AI work behind the .NET API, not in Next.js routes.
- Treat the model like a flaky third-party service: timeouts, retries, validated output, a fallback.
- For anything slow, go async with status — don't make the user hold a connection open.
- Use no-code runners for service glue, custom code for anything touching your domain.
AI automation isn't a feature you sprinkle on top. It's a dependency you architect around — and on a .NET + Next.js stack, there's a clear right place for every piece of it.