Skip to content
Kamran Code

How to Integrate AI Into an Existing Web Application

You do not need a rewrite to add AI to a Laravel or Next.js application. A step-by-step approach: find the right feature, isolate the model behind a service, validate outputs, handle cost and failure, and ship behind a flag.

Author
Kamran Khan
Published
Reading time
7 min read
On this page
  1. Step 1: Choose a feature, not a strategy
  2. Step 2: Put the model behind a service boundary
  3. Step 3: Make outputs structured and validated
  4. Step 4: Decide where it runs — request or job
  5. Step 5: Ground it in your data where it matters
  6. Step 6: Handle cost, failure and abuse
  7. Step 7: Ship behind a flag, measure, then widen
  8. What this looks like in practice
  9. Mistakes to avoid
  10. Summary

Most businesses that want AI already have an application — a Laravel back office, a Next.js product, a customer portal — full of real data and real workflows. The question is not "how do we build an AI product?" but "how do we add AI to this without breaking it, without a rewrite and without a bill nobody signed off?"

This is the approach I use. It works for a ten-year-old PHP monolith and for a modern TypeScript app, because the principles are the same.

Step 1: Choose a feature, not a strategy

The first AI feature should be:

  • Narrow — one input, one output, one place in the interface.
  • Reversible — if it is wrong, a person can ignore or edit the result.
  • Frequent — used often enough to learn from within weeks.
  • Measurable — you can tell whether it saved time or improved quality.

Good first features in existing applications:

  • Summarise a long record (a ticket thread, a customer history, a case file) at the top of the page.
  • Draft a reply, a description or a report from existing fields.
  • Classify or tag incoming items (enquiries, documents, reviews).
  • Extract structured fields from free text (an address from an email, line items from an invoice).
  • Natural-language search over a list the user already filters manually.

Poor first features: a general-purpose chatbot, anything that sends or pays automatically, anything the business cannot describe as a sentence with a verb.

Step 2: Put the model behind a service boundary

Do not scatter API calls to a model provider through controllers. Create one service in your application that owns all model interaction:

// app/Services/AI/Completion.php (Laravel)
final class Completion
{
    public function __construct(private ProviderClient $client, private Metrics $metrics) {}
 
    /** @template T  @param class-string<T> $schema  @return T */
    public function structured(string $task, string $input, string $schema, Model $model = Model::Fast)
    {
        $prompt = PromptRegistry::get($task);           // versioned prompts, not inline strings
        $response = $this->client->generate($model, $prompt->system(), $input, $schema::jsonSchema());
        $this->metrics->record($task, $model, $response->usage());
        return $schema::fromArray($response->json()); // throws on invalid shape
    }
}

This boundary gives you, in one place: provider abstraction (swap vendors without touching features), prompt versioning, usage metering, timeouts and retries, and a single point to mock in tests. Every feature calls Completion, never the vendor SDK.

In a Next.js application the same idea is a module in lib/ai/ used only from server code — route handlers, server actions or server components. Model API keys never reach the browser.

Step 3: Make outputs structured and validated

If the model's output feeds any code path — saving a field, choosing a route, triggering a job — it must be a schema, not prose.

// lib/ai/schemas.ts (Next.js)
export const EnquiryClassification = z.object({
  category: z.enum(["sales", "support", "billing", "spam"]),
  confidence: z.number().min(0).max(1),
  summary: z.string().max(160),
});
 
// app/api/enquiries/classify/route.ts
const result = await ai.structured("classify-enquiry", body.text, EnquiryClassification);
// result is typed and validated; anything malformed threw before reaching here

Design the schema so a low-confidence or uncertain result has somewhere to go — an other category, a needs_review flag — rather than forcing the model to guess. Then handle that case in the UI as a normal state, not an error.

Step 4: Decide where it runs — request or job

Model calls take one to several seconds and occasionally fail. Two placements:

In the request for interactive features where the user is waiting and the result is small: a summary on page load (cached), a drafted reply on button click. Set a hard timeout, show a loading state and a graceful fallback ("Summary unavailable — showing full thread").

In a background job for anything that touches many records, runs on a schedule or should not block a user: classifying the overnight batch of enquiries, extracting data from uploaded documents, enriching new CRM records. Store the result on the record with the model version and timestamp, and let the UI display it when ready.

Existing applications already have a queue (Laravel queues, BullMQ, a cron). Use it. Do not build a second one for AI.

Step 5: Ground it in your data where it matters

A summary or draft only needs the record in front of it. A question-answering feature needs retrieval over your content. The incremental path:

  1. Start with just the record: pass the relevant fields and history in the prompt. No vector database needed.
  2. If answers need wider context — policies, product docs, past cases — add retrieval as a separate service that returns ranked, permission-filtered passages, and include them in the prompt. This is the RAG pattern; PostgreSQL with pgvector keeps it inside the database you already run.
  3. Only if the feature must act — look things up dynamically, create records — add tools and an agent loop, with human approval on writes.

Most first features stop at step 1. That is a good thing.

Step 6: Handle cost, failure and abuse

Before the feature reaches users:

  • Meter usage per feature and per tenant in the service boundary. Put the number on a dashboard.
  • Cache deterministic results: the summary of an unchanged record does not need regenerating on every view. Key the cache on the input's hash and the prompt version.
  • Rate limit interactive features per user.
  • Set a budget and a behaviour when it is exceeded — degrade to the non-AI path, don't fail the page.
  • Treat provider outages as normal. Timeouts, one retry with backoff, then fallback. The application must work with the AI feature turned off.
  • Never pass secrets or other users' data into a prompt. Build the input from the current user's permitted data only, the same as you would for an API response.

Step 7: Ship behind a flag, measure, then widen

Release to a small group with a feature flag. Collect:

  • Usage: how often the feature is invoked.
  • Acceptance: how often a draft is used as-is, edited or discarded; how often a classification is corrected.
  • Time: before/after on the task it was meant to speed up.
  • Cost per use.

Corrections are gold — they become the evaluation set that lets you improve prompts and models with evidence. Store them.

Widen the rollout when acceptance is high and cost is understood. Remove the flag when nobody remembers the feature being new.

What this looks like in practice

For a Laravel support system I'd add, in order: a "summarise thread" button (request-time, cached), overnight classification of new tickets (queued job, structured output, needs_review bucket), then suggested replies grounded in resolved tickets (retrieval over the ticket archive, human sends). Three features, each independently useful, no rewrite, and each one built on the service boundary and the data from the last.

For a Next.js product the same sequence uses server actions for the interactive pieces, route handlers plus a queue for batch work, and the existing PostgreSQL database with pgvector for retrieval.

Mistakes to avoid

  • Calling the vendor SDK from everywhere. You will want to change models within a year.
  • Trusting model JSON without parsing. It looks right until it isn't.
  • A chatbot as the first feature. Hardest to make reliable, least likely to fit the workflow.
  • No fallback when the model is down. The application must degrade to what it did before.
  • No measurement. Without acceptance and cost data you cannot justify the second feature — or kill the first one.

Summary

Integrating AI into an existing application is an incremental engineering task: pick a narrow, reversible, measurable feature; route every model call through one service with validation, metering and fallbacks; run it in the request or the queue as the feature demands; ground it in your data only as far as needed; and ship behind a flag with the numbers to prove it earned its place. Done this way, the first feature takes weeks, not quarters, and it leaves the application better structured for the next one.

  • #ai-integration
  • #laravel
  • #nextjs
  • #llm
  • #structured-output
  • #feature-flags
  • #production

Kamran Khan

Software Engineer and AI Application Developer working across full-stack development, business systems, automation and production AI.

About Kamran →

Keep reading

  • SaaS7 min read

    How to Design a Scalable SaaS Architecture

    Tenancy, billing, background work, environments and the boundaries that let a SaaS product grow without a rewrite. A practical architecture for a first version that is built to last, with Next.js, Laravel and PostgreSQL as the reference stack.

    • #saas
    • #architecture
    • #multi-tenancy
    • #nextjs
  • AI Engineering8 min read

    How to Build an AI-Powered Business Application

    A practical architecture for AI applications that do real work inside a business: where the model sits, how it reaches your data and tools, and what has to be true before it goes to production.

    • #ai-applications
    • #llm
    • #architecture
    • #rag
  • CRM8 min read

    How to Build a Custom CRM

    When a generic CRM stops fitting, a custom one is often smaller than people expect. The data model, pipeline design, integrations and reporting that make a custom CRM worth building — and the signs that you should not.

    • #crm
    • #business-systems
    • #laravel
    • #data-modelling

Turn an AI idea into working software.

From retrieval and agents to document intelligence, I build AI systems that plug into real business data and workflows.