A job agent inside Next.js

Next.js is an excellent front door for an agent and a poor place to run one. Knowing where the line falls saves a rewrite.

JobsDart Editorial5 min read

Key takeaways

  • The interface, auth and endpoints belong here; the agent run does not.
  • Every provider call goes through a route handler, which is also the only enforceable limit point.
  • Streaming makes a slow response tolerable and does not make a long process possible.
  • The execution limit is the boundary: enqueue and return an identifier.
  • Keep agent code free of framework imports so it can move without a rewrite.

What belongs in the app

Next.js handles the parts users touch: the interface, authentication, the endpoints that start and monitor agent work, and the streaming of visible output. All of that fits naturally and benefits from server components and route handlers.

What does not belong is the agent run itself. A process that browses sites, waits for approval and continues tomorrow is not a request, and trying to hold it in one is where the design goes wrong.

Server components are a genuine advantage for the read side. Rendering an application list or a match explanation on the server keeps the data access and the credentials there, and ships the user markup rather than a client that has to be trusted with a query.

Keys and calls stay on the server

Provider credentials must never reach the browser, which means no direct client-to-provider calls, no keys in public environment variables, and no assumption that a variable is private because it is not referenced in client code.

Route handlers are the boundary. They authenticate the user, apply quota, call the provider and stream results back. That also gives you the only place where rate limiting and abuse controls can actually be enforced.

Resolve identity from the session rather than from anything the request carries. A candidate id in a request body is a claim the client controls, and scoping a query by it is the mistake that turns a normal endpoint into a way to read someone else’s applications.

  • Provider keys in server-only environment variables
  • Every model call made from a route handler, never the client
  • Identity resolved from the session, not from the request body
  • Quota and rate limits applied at that same boundary
Where each piece runs
PieceWhereWhy
Interface and authNext.jsWhat it is for
Start or monitor a runRoute handlerShort, authenticated
A single scoring callRoute handlerFits the execution limit
Assistant chat streamingRoute handlerStreaming is well supported
A multi-site agent runWorkerExceeds the limit by far
Anything awaiting approvalWorker plus queueNot a request at all

Streaming works well, within limits

Streaming a response from a route handler to a client component is well supported and makes an assistant feel responsive. For a chat-style career assistant this is the right shape.

It is still a request, though, and it is bounded by the platform’s execution limit. Streaming makes a slow response tolerable; it does not make a long-running process possible.

Handle disconnection deliberately. A user closing the tab mid-stream should abort the provider call rather than leaving it running to completion, because an abandoned stream that still bills is a cost with no corresponding value.

The execution limit is the boundary

Serverless functions are cut off at a fixed duration. A single scoring call fits comfortably. An agent applying to eight jobs across four sites with a human approval step in the middle does not, and will be terminated mid-run.

So the route handler enqueues work and returns an identifier. A separate worker — a container, a queue consumer, a scheduled process — performs the run, and the client polls or subscribes for progress.

Browser automation is the clearest case for leaving entirely. A headless browser needs memory and startup time that serverless functions are poorly suited to, so that work belongs in a long-lived container from the first version rather than after the first timeout.

Caching and revalidation, carefully

The framework’s caching is a real advantage for job listings and static content, and a hazard for anything candidate-specific. A personalised feed cached at the wrong layer is one user seeing another user’s recommendations, which is a data leak dressed as a performance win.

Mark candidate-scoped routes as dynamic explicitly rather than relying on inference. Inference changes between versions and depends on which functions a component happens to call, which is not a foundation for a decision about who sees what.

Revalidate on the events that matter rather than on a timer. A new application, a changed target or a fresh batch of postings should invalidate the affected pages directly, which is both more accurate and cheaper than a short interval that mostly regenerates unchanged content.

  • Public listings cached; candidate-scoped routes explicitly dynamic
  • Never cache a response whose content depends on the session
  • Revalidate on events, not on a short timer
  • Test cache behaviour with two accounts, not one

Keep the agent out of the app’s own code

Even when both live in one repository, keep the agent as a module with no Next.js imports. It should run from a worker process, a test or a script without a framework present.

That separation is what lets you move execution later without rewriting the logic, and it makes the agent testable without spinning up an application. Agent code tangled with request objects and framework helpers is the version nobody can move.

Share the types across the boundary rather than the runtime. A monorepo where the agent and the application use the same domain types keeps them consistent without the agent depending on the framework, and it is the arrangement that stays workable as both grow.

Frequently asked questions

Can I run an AI agent inside Next.js route handlers?

Short tasks yes, full agent runs no. Serverless execution limits terminate a run that browses several sites and waits for approval mid-way.

Where do provider API keys go?

Server-only environment variables, with every model call made from a route handler. That boundary is also the only place quota and rate limits can be enforced.

Is streaming enough for a long agent run?

No. Streaming makes a slow response tolerable but it is still a request bounded by the execution limit. Long runs need a queue and a worker.

Should agent code import from Next.js?

No. Keep it as a module runnable from a worker, a test or a script, sharing domain types rather than the framework runtime.

What is the caching hazard here?

Caching a candidate-scoped route, which shows one user another user recommendations. Mark those dynamic explicitly rather than relying on inference.

Where should browser automation run?

In a long-lived container from the first version. A headless browser needs memory and startup time that serverless functions handle badly.

Further reading

Check this against your own resume

Scan your CV against a real job description, or build a parse-safe one from scratch. Your first scan costs nothing.

Keep reading

All career guides