The Dev Log › AI & Machine Learning
How to Add an AI Chatbot to Your Laravel App
By Jezer Niel Blanca, Full Stack Developer ·
·
4 min read
A clean, production-minded way to wire an LLM-powered chatbot into Laravel: config, a service class, rate limiting, and a Vue front end.
Adding an AI chatbot to a Laravel app is easier than most people expect. The hard parts aren't the API call; they're keeping your keys safe, controlling costs, and giving users a response that feels fast. Here's the approach I use, built only on Laravel's HTTP client so it works with whichever LLM provider you choose.
Keep Your Keys in Config, Not Code
Never hardcode an API key, and never call env() outside of config files. Add a dedicated entry to config/services.php:
'llm' => [
'key' => env('LLM_API_KEY'),
'url' => env('LLM_API_URL', 'https://api.example.com/v1/messages'),
'model' => env('LLM_MODEL', 'your-model-id'),
'max_tokens' => (int) env('LLM_MAX_TOKENS', 1024),
],
Then set the values in .env and make sure .env never reaches version control. With config caching in production, this also keeps things fast.
A Small Service Class
I wrap the provider call in a single class so controllers stay thin and swapping providers later is a one-file change:
namespace App\Services;
use Illuminate\Support\Facades\Http;
class ChatService
{
/**
* @param array<int, array{role: string, content: string}> $messages
*/
public function reply(array $messages, string $systemPrompt): string
{
$response = Http::withToken(config('services.llm.key'))
->acceptJson()
->timeout(30)
->retry(2, 500)
->post(config('services.llm.url'), [
'model' => config('services.llm.model'),
'max_tokens' => config('services.llm.max_tokens'),
'system' => $systemPrompt,
'messages' => $messages,
])
->throw();
return $response->json('content.0.text', '');
}
}
Payload shapes differ slightly between providers, so check your provider's docs for the exact request and response format. The structure above, a system prompt plus an array of role/content messages, is common to most of them.
Validate Input with a Form Request
User input going to an LLM still needs validation. Limit message length and history size so a single request can't blow up your token bill:
public function rules(): array
{
return [
'messages' => ['required', 'array', 'max:20'],
'messages.*.role' => ['required', 'in:user,assistant'],
'messages.*.content' => ['required', 'string', 'max:2000'],
];
}
Rate Limit Before You Launch
An open chatbot endpoint is an invitation to abuse. Laravel's rate limiter makes this a few lines in AppServiceProvider::boot():
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('chat', function (Request $request) {
return Limit::perMinute(10)->by($request->user()?->id ?: $request->ip());
});
Attach it to the route:
Route::post('/chat', ChatController::class)
->middleware('throttle:chat')
->name('chat.send');
Rate limiting isn't just about security. It's the simplest cost control you have.
Ground the Bot with a System Prompt
A generic model knows nothing about your product. The system prompt is where you give it a role, context, and boundaries:
- Who it is and who it's talking to
- What it should and shouldn't answer
- Key facts about your product, pricing, or services
- The tone and length of responses
I keep system prompts in a config file or a database table so they can be adjusted without a deploy.
Streaming for a Faster Feel
LLM responses can take several seconds. Streaming tokens as they arrive makes the wait feel much shorter. Laravel supports streamed responses natively with response()->stream(), and most providers offer a server-sent events mode. On the front end, you read the stream chunk by chunk:
const response = await fetch('/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-XSRF-TOKEN': token },
body: JSON.stringify({ messages }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
reply.value += decoder.decode(value, { stream: true });
}
Start without streaming if you want to ship quickly, then add it once the basic flow works.
A Minimal Vue Front End
On the client, the chat component only needs a list of messages, an input, and a loading state:
<script setup>
import { ref } from 'vue';
import axios from 'axios';
const messages = ref([]);
const draft = ref('');
const isSending = ref(false);
async function send() {
if (!draft.value.trim()) return;
messages.value.push({ role: 'user', content: draft.value });
draft.value = '';
isSending.value = true;
try {
const { data } = await axios.post(route('chat.send'), { messages: messages.value });
messages.value.push({ role: 'assistant', content: data.reply });
} finally {
isSending.value = false;
}
}
</script>
Production Checklist
- Log token usage per request so you can see costs by user or feature.
- Handle provider errors gracefully with a friendly fallback message.
- Never send secrets or other users' data in the prompt.
- Cache answers to frequent questions where it makes sense.
- Write feature tests with
Http::fake() so your suite never calls the real API.
Have an idea for an AI feature in your product? Let's talk. I enjoy turning "wouldn't it be cool if" into something users actually rely on.
Tags: Laravel, AI, Chatbot, LLM