Skip to main content

Laravel Usage Guide

8 min 1,667 words

Notes written while learning Laravel from a tech intern handover doc. The codebase uses Laravel as the backend layer for handling business logic, API routes, webhooks, and integration with external services.


Table of Contents

1. What is Laravel?

Laravel is a modern PHP framework for building secure, scalable web applications. It was created by Taylor Otwell and follows the MVC (Model-View-Controller) architecture pattern.

Instead of writing raw PHP from scratch, Laravel gives you a structured way to organize your code and a rich set of built-in tools so you don’t have to reinvent the wheel:

  • Routing: map URLs to code
  • Authentication: login, registration, sessions out of the box
  • Database management: interact with databases using plain PHP objects instead of raw SQL
  • Job queues: run heavy tasks in the background
  • HTTP Client: call external APIs cleanly

In the context of this codebase, Laravel acts as the backend layer responsible for:

  • Handling API routes and webhook requests (e.g., from Twilio)
  • Processing business logic in controllers and services
  • Managing user sessions and form submissions
  • Talking to the database via Eloquent ORM
  • Dispatching background jobs for async processing

2. MVC Architecture

MVC stands for Model, View, Controller. It is a design pattern that separates your application into three distinct layers so that each piece of code has a clear, single responsibility.

Browser Request
      |
   Router
      |
  Controller  <---->  Model (database)
      |
    View (HTML / JSON response)

Model

The Model represents your data and the rules around it. It talks directly to the database. In Laravel, models use Eloquent ORM, which lets you work with database rows as PHP objects.

// A User model represents one row in the `users` table
$user = User::find(1);
echo $user->name; // No SQL needed

View

The View is what the user sees: the HTML template or JSON response. In API-only apps (like this codebase), views are usually just JSON responses rather than HTML pages.

return response()->json(['status' => 'ok', 'message' => 'Webhook received']);

Controller

The Controller is the middleman. It receives the request, talks to models and services to get or process data, then returns a response.

class ChatbotController extends Controller
{
    public function handle(Request $request)
    {
        $message = $request->input('Body');  // get data from request
        $reply = $this->chatbotService->respond($message);  // business logic
        return response()->json(['reply' => $reply]);  // send response
    }
}

The rule of thumb: keep controllers thin. Heavy logic belongs in Services, not controllers.


3. Project Structure

When you open a Laravel project, the key folders you need to know:

app/
|--  Http/
|   |--  Controllers/    # Controllers live here
|   |--  Middleware/     # Request filters (auth, logging, etc.)
|--  Models/             # Eloquent models (one per database table)
|--  Services/           # Business logic classes
|--  Jobs/               # Async background jobs
config/                  # App configuration (database, queue, services)
database/
|--  migrations/         # Database schema version control
routes/
|--  web.php             # Browser routes (with sessions, CSRF)
|--  api.php             # API routes (stateless, /api prefix)
.env                     # Environment variables (secrets, DB credentials)

Never commit your .env file. It contains sensitive credentials like database passwords and API keys.


4. Request Lifecycle

This is one of the most important things to understand. When a request hits your Laravel app (a webhook, a form submission, or an API call), here is exactly what happens:

Step 1: Request enters through public/index.php

Every single request, regardless of URL, enters through one file: public/index.php. The web server (Nginx/Apache) is configured to route all traffic here. This file boots the framework.

Step 2: The framework boots

Laravel loads all its service providers and configurations. Think of this as the app waking up and getting everything ready before handling your request.

Step 3: Middleware runs

Before the request ever reaches your controller, it passes through a middleware stack. Middleware is like a series of checkpoints:

  • Is the app in maintenance mode? Block the request.
  • Is the user authenticated? If not, redirect to login.
  • Is the CSRF token valid? If not, reject it.

Step 4: Router matches the route

The router looks at the incoming URL and HTTP method (GET, POST, etc.) and finds the matching route definition in routes/api.php or routes/web.php.

// routes/api.php
Route::post('/webhook', [ChatbotController::class, 'handle']);

Step 5: Controller executes

The matched route calls the appropriate controller method. The controller:

  1. Reads input from the request
  2. Calls services or models for data/logic
  3. May dispatch a background job
  4. Returns a response (JSON, view, redirect)
public function handle(Request $request)
{
    $data = $request->all();            // 1. read input
    $result = $this->service->process($data);  // 2. business logic
    ProcessWebhookJob::dispatch($data); // 3. async job (optional)
    return response()->json($result);   // 4. return response
}

Step 6: Response travels back

The response goes back through the middleware stack (some middleware acts on the way out too), and is finally sent to the client.


5. Routing

Routes are defined in the routes/ folder. There are two main route files:

FileUsed forFeatures
routes/web.phpBrowser pagesSessions, cookies, CSRF protection
routes/api.phpAPI endpointsStateless, auto-prefixed with /api

Basic routes

use Illuminate\Support\Facades\Route;

// GET /api/users
Route::get('/users', function () {
    return response()->json(['users' => []]);
});

// POST /api/webhook
Route::post('/webhook', [ChatbotController::class, 'handle']);

// PUT /api/users/{id}
Route::put('/users/{id}', [UserController::class, 'update']);

// DELETE /api/users/{id}
Route::delete('/users/{id}', [UserController::class, 'destroy']);

Route parameters

Capture dynamic segments from the URL using {curly braces}:

// /api/users/42  ->  $id = '42'
Route::get('/users/{id}', function (string $id) {
    return User::findOrFail($id);
});

// Optional parameter with default
Route::get('/posts/{status?}', function (string $status = 'published') {
    return Post::where('status', $status)->get();
});

Route groups

Apply shared settings (like middleware or a URL prefix) to multiple routes at once:

// All routes in this group require authentication
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::put('/profile', [ProfileController::class, 'update']);
});

// All routes share the /admin prefix
Route::prefix('admin')->group(function () {
    Route::get('/users', [AdminController::class, 'users']);
    Route::get('/stats', [AdminController::class, 'stats']);
});

RESTful resource routes

Instead of defining 7 CRUD routes manually, one line does it all:

Route::apiResource('messages', MessageController::class);

This creates:

MethodURLController method
GET/api/messagesindex
POST/api/messagesstore
GET/api/messages/{id}show
PUT/api/messages/{id}update
DELETE/api/messages/{id}destroy

Useful Artisan command

php artisan route:list          # see all registered routes
php artisan route:list --path=api  # filter to API routes only

6. Controllers

Controllers live in app/Http/Controllers/. Each controller groups related request handling logic.

Creating a controller

php artisan make:controller ChatbotController

Basic controller

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class ChatbotController extends Controller
{
    public function handle(Request $request): JsonResponse
    {
        // Validate incoming data
        $validated = $request->validate([
            'Body' => 'required|string',
            'From' => 'required|string',
        ]);

        // Process and return
        return response()->json([
            'status' => 'received',
            'from'   => $validated['From'],
        ]);
    }
}

Dependency injection in controllers

Laravel’s service container automatically injects dependencies when you type-hint them in the constructor or method:

class ChatbotController extends Controller
{
    // Laravel automatically creates and injects ChatbotService
    public function __construct(
        private ChatbotService $chatbotService
    ) {}

    public function handle(Request $request): JsonResponse
    {
        $reply = $this->chatbotService->respond($request->input('Body'));
        return response()->json(['reply' => $reply]);
    }
}

Request validation

Always validate incoming data before using it:

$validated = $request->validate([
    'email'    => 'required|email',
    'password' => 'required|min:8',
    'name'     => 'sometimes|string|max:255',
]);

// If validation fails, Laravel automatically returns a 422 error response
// If it passes, $validated contains only the declared fields

Common validation rules:

RuleMeaning
requiredField must be present and not empty
stringMust be a string
emailMust be a valid email format
min:8Minimum length of 8
max:255Maximum length of 255
integerMust be an integer
nullableCan be null
sometimesOnly validate if the field is present

7. Models and Eloquent ORM

Eloquent is Laravel’s ORM (Object Relational Mapper). It lets you interact with your database tables using PHP classes instead of writing raw SQL.

Convention: one model = one database table.

php artisan make:model Message
# Creates app/Models/Message.php
# Model name 'Message' maps to 'messages' table by convention

Defining a model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Message extends Model
{
    // Columns that can be mass-assigned (security measure)
    protected $fillable = ['from', 'body', 'status', 'session_id'];

    // Type casting
    protected $casts = [
        'created_at' => 'datetime',
        'is_read'    => 'boolean',
    ];
}

CRUD operations

// Create
$message = Message::create([
    'from'   => '+6591234567',
    'body'   => 'Hello',
    'status' => 'received',
]);

// Read
$message  = Message::find(1);               // by ID (null if not found)
$message  = Message::findOrFail(1);         // by ID (throws 404 if not found)
$messages = Message::all();                  // all records
$messages = Message::where('status', 'received')->get();  // filtered
$latest   = Message::latest()->first();      // most recent record

// Update
$message->update(['status' => 'processed']);
// or
$message->status = 'processed';
$message->save();

// Delete
$message->delete();

// Mass delete
Message::where('status', 'spam')->delete();

Query builder

Chain methods to build queries:

$messages = Message::query()
    ->where('status', 'received')
    ->where('created_at', '>=', now()->subHour())
    ->orderBy('created_at', 'desc')
    ->limit(50)
    ->get();

Relationships

// A User has many Messages
class User extends Model
{
    public function messages()
    {
        return $this->hasMany(Message::class);
    }
}

// A Message belongs to a User
class Message extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

// Usage
$user = User::find(1);
$messages = $user->messages;  // all messages belonging to this user

$message = Message::find(1);
$user = $message->user;       // the user who owns this message

8. Database Migrations

Migrations are like version control for your database schema. Instead of manually creating tables in a database GUI, you write PHP code that creates or modifies tables. This means every developer on the team can reproduce the exact same database structure.

php artisan make:migration create_messages_table

This creates a file in database/migrations/:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->id();                        // auto-increment primary key
            $table->string('from');              // VARCHAR
            $table->text('body');                // TEXT
            $table->string('status')->default('received');
            $table->foreignId('user_id')->constrained(); // foreign key
            $table->boolean('is_read')->default(false);
            $table->timestamps();                // created_at, updated_at
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('messages'); // rollback
    }
};

Run migrations:

php artisan migrate             # run pending migrations
php artisan migrate:rollback    # undo last batch
php artisan migrate:fresh       # drop all tables and re-run (dev only)

9. Services

A Service is a plain PHP class that holds business logic. Controllers should be thin: they delegate the actual work to services.

Why not put everything in the controller? Because:

  • Controllers get bloated fast
  • Logic in services can be reused across multiple controllers
  • Services are easier to test in isolation
# No artisan command for services: just create the file manually
# app/Services/ChatbotService.php
<?php

namespace App\Services;

use App\Models\Message;

class ChatbotService
{
    public function respond(string $incomingMessage, string $from): string
    {
        // Save the incoming message
        Message::create([
            'from'   => $from,
            'body'   => $incomingMessage,
            'status' => 'received',
        ]);

        // Business logic: generate a reply
        $reply = $this->generateReply($incomingMessage);

        // Save the outgoing message
        Message::create([
            'from'   => 'bot',
            'body'   => $reply,
            'status' => 'sent',
        ]);

        return $reply;
    }

    private function generateReply(string $message): string
    {
        // Your AI/logic here
        return 'Thanks for your message: ' . $message;
    }
}

Use it in the controller:

class ChatbotController extends Controller
{
    public function __construct(private ChatbotService $chatbotService) {}

    public function handle(Request $request): JsonResponse
    {
        $reply = $this->chatbotService->respond(
            $request->input('Body'),
            $request->input('From')
        );
        return response()->json(['reply' => $reply]);
    }
}

10. Jobs and Queues (Async Processing)

Some tasks are too slow to run during a web request: sending emails, calling an AI API, processing files. You don’t want the user to wait. Jobs let you push that work into a background queue and return a response immediately.

Request --> Controller --> dispatch(Job) --> return response immediately
                                |
                         Queue Worker picks it up
                                |
                         Job runs in the background

Creating a job

php artisan make:job ProcessWebhookJob
<?php

namespace App\Jobs;

use App\Services\ChatbotService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class ProcessWebhookJob implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public string $from,
        public string $body
    ) {}

    public function handle(ChatbotService $chatbotService): void
    {
        // This runs in the background
        $chatbotService->respond($this->body, $this->from);
    }
}

Dispatching a job

// In your controller -- fire and forget
ProcessWebhookJob::dispatch($from, $body);

// Delayed dispatch (run after 5 minutes)
ProcessWebhookJob::dispatch($from, $body)->delay(now()->addMinutes(5));

// Dispatch to a specific queue
ProcessWebhookJob::dispatch($from, $body)->onQueue('webhooks');

Running the queue worker

php artisan queue:work                    # process jobs continuously
php artisan queue:work --queue=webhooks   # process specific queue

In production, use Supervisor to keep the queue worker running persistently.


11. Calling External APIs

Laravel’s HTTP Client is a clean wrapper around Guzzle that makes calling external APIs (like Twilio or AWS Bedrock) straightforward.

use Illuminate\Support\Facades\Http;

Basic requests

// GET request
$response = Http::get('https://api.example.com/users');

// POST request with JSON body (default)
$response = Http::post('https://api.example.com/messages', [
    'to'   => '+6591234567',
    'body' => 'Hello from Laravel',
]);

// With authentication header (Bearer token)
$response = Http::withToken(config('services.openai.key'))
    ->post('https://api.openai.com/v1/chat/completions', [
        'model'    => 'gpt-4',
        'messages' => [['role' => 'user', 'content' => 'Hello']],
    ]);

Reading the response

$response->successful();    // true if status is 2xx
$response->failed();        // true if status is 4xx or 5xx
$response->status();        // e.g. 200, 404, 500
$response->json();          // parse body as array
$response['key'];           // access JSON key directly
$response->body();          // raw string body

Error handling and retries

$response = Http::timeout(10)          // fail if no response in 10 seconds
    ->retry(3, 500)                    // retry 3 times, wait 500ms between tries
    ->post('https://api.twilio.com/...', $payload);

if ($response->failed()) {
    Log::error('Twilio API call failed', [
        'status'   => $response->status(),
        'response' => $response->body(),
    ]);
    throw new \Exception('Failed to send message');
}

A real-world example: Twilio

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;

class TwilioService
{
    private string $accountSid;
    private string $authToken;
    private string $fromNumber;

    public function __construct()
    {
        $this->accountSid = config('services.twilio.sid');
        $this->authToken  = config('services.twilio.token');
        $this->fromNumber = config('services.twilio.from');
    }

    public function sendMessage(string $to, string $body): bool
    {
        $response = Http::withBasicAuth($this->accountSid, $this->authToken)
            ->asForm()
            ->post("https://api.twilio.com/2010-04-01/Accounts/{$this->accountSid}/Messages.json", [
                'From' => $this->fromNumber,
                'To'   => $to,
                'Body' => $body,
            ]);

        return $response->successful();
    }
}

Store credentials in .env, never in code:

# .env
TWILIO_SID=ACxxxxxxxxxxxxxxxx
TWILIO_TOKEN=your_auth_token
TWILIO_FROM=+1234567890
// config/services.php
'twilio' => [
    'sid'   => env('TWILIO_SID'),
    'token' => env('TWILIO_TOKEN'),
    'from'  => env('TWILIO_FROM'),
],

12. Webhooks and Form Submissions

A webhook is a POST request sent by an external service to your app when something happens (e.g., Twilio sends a webhook when a user sends an SMS to your number).

Receiving a webhook

// routes/api.php
Route::post('/webhook/twilio', [ChatbotController::class, 'handle']);
// app/Http/Controllers/ChatbotController.php
public function handle(Request $request): JsonResponse
{
    // Twilio sends form data, not JSON
    $from = $request->input('From');  // sender's phone number
    $body = $request->input('Body');  // message content

    // Dispatch to background queue so we return instantly
    ProcessWebhookJob::dispatch($from, $body);

    // Twilio expects a quick 200 response
    return response()->json(['status' => 'queued'], 200);
}

CSRF protection for webhooks

API routes (in routes/api.php) are stateless and do not have CSRF protection by default, which is what you want for webhooks. Web routes (in routes/web.php) do have CSRF protection.

If you need to exempt a specific web route from CSRF (e.g., a third-party callback):

// bootstrap/app.php (Laravel 12)
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'stripe/webhook',
        'twilio/callback',
    ]);
})

Form submissions

For browser-submitted HTML forms (in routes/web.php), always include the CSRF token:

<form method="POST" action="/contact">
    @csrf
    <input type="text" name="name" />
    <input type="email" name="email" />
    <button type="submit">Send</button>
</form>
public function store(Request $request)
{
    $validated = $request->validate([
        'name'  => 'required|string|max:255',
        'email' => 'required|email',
    ]);

    // Process form data...
    return redirect()->back()->with('success', 'Message sent!');
}

13. Middleware

Middleware filters requests before they reach your controller. Think of it as a checkpoint system.

Common built-in middleware:

MiddlewareWhat it does
authBlocks unauthenticated users
auth:sanctumAPI token authentication
throttle:60,1Rate limiting (60 requests per minute)
verifiedRequires email verification

Applying middleware to routes

// Single route
Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth');

// Route group
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/settings', [SettingsController::class, 'index']);
});

// With rate limiting for webhooks
Route::post('/webhook', [ChatbotController::class, 'handle'])
    ->middleware('throttle:100,1');

Creating custom middleware

php artisan make:middleware VerifyTwilioSignature
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class VerifyTwilioSignature
{
    public function handle(Request $request, Closure $next)
    {
        $signature = $request->header('X-Twilio-Signature');

        if (!$this->isValidSignature($signature, $request)) {
            return response()->json(['error' => 'Invalid signature'], 403);
        }

        return $next($request);  // pass request to the next layer
    }

    private function isValidSignature(string $signature, Request $request): bool
    {
        // Your validation logic here
        return true;
    }
}

14. Authentication

Laravel makes authentication straightforward. For API-based apps, Laravel Sanctum is the standard.

Install Sanctum

php artisan install:api
php artisan migrate

Issuing API tokens

// When a user logs in, issue a token
public function login(Request $request)
{
    $credentials = $request->validate([
        'email'    => 'required|email',
        'password' => 'required',
    ]);

    if (!Auth::attempt($credentials)) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }

    $user  = Auth::user();
    $token = $user->createToken('api-token')->plainTextToken;

    return response()->json(['token' => $token]);
}

// Logout: revoke the token
public function logout(Request $request)
{
    $request->user()->currentAccessToken()->delete();
    return response()->json(['message' => 'Logged out']);
}

Protecting routes

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());
    Route::post('/messages', [MessageController::class, 'store']);
});

Making authenticated API requests

Clients send the token in the Authorization header:

Authorization: Bearer 1|abc123...

15. Artisan CLI

Artisan is Laravel’s built-in command-line tool. You will use it constantly.

# Development
php artisan serve               # start local development server
php artisan tinker              # interactive PHP REPL (great for testing queries)

# Code generation
php artisan make:controller NameController
php artisan make:model Name --migration   # model + migration together
php artisan make:job ProcessSomething
php artisan make:middleware CheckHeader
php artisan make:request StoreMessageRequest

# Database
php artisan migrate             # run pending migrations
php artisan migrate:rollback    # undo last migration batch
php artisan migrate:fresh       # drop everything and re-migrate (dev only)
php artisan db:seed             # run database seeders

# Routes
php artisan route:list          # see all routes
php artisan route:list --path=api

# Cache
php artisan cache:clear         # clear application cache
php artisan config:clear        # clear config cache
php artisan optimize:clear      # clear all caches

# Queue
php artisan queue:work          # start processing jobs
php artisan queue:failed        # list failed jobs
php artisan queue:retry all     # retry all failed jobs

16. Best Practices and Common Mistakes

Keep controllers thin

Controllers should only:

  1. Validate the request
  2. Call a service or model
  3. Return a response

Heavy logic belongs in Services.

Never trust user input

Always validate before using:

// Never do this
$name = $request->input('name');
User::create(['name' => $name]); // dangerous if name is malicious

// Always do this
$validated = $request->validate(['name' => 'required|string|max:255']);
User::create($validated);

Store secrets in .env, not in code

// Never hardcode credentials
$apiKey = 'sk-abc123...'; // exposed if someone reads your code

// Always use .env
$apiKey = config('services.openai.key');

Common mistakes table

MistakeWhat goes wrongHow to fix
Logic in controllersControllers become unmaintainableMove to a Service class
Missing $fillable on modelMass assignment vulnerabilityDefine $fillable on every model
Committing .env to gitCredentials exposedAdd .env to .gitignore
No validation on inputSecurity vulnerabilitiesAlways call $request->validate()
N+1 query problemHundreds of unnecessary DB queriesUse eager loading: User::with('messages')->get()
Forgetting php artisan optimize:clearOld config/routes loadedClear caches after config changes
Running heavy logic synchronouslySlow response timesUse Jobs for anything that takes time

Quick Reference

ConceptWhat it does
RouteMaps a URL and HTTP method to a controller
ControllerHandles the request and returns a response
ModelRepresents a database table as a PHP class
MigrationVersion-controlled database schema change
ServiceClass containing reusable business logic
JobBackground task run asynchronously via a queue
MiddlewareFilters requests before they hit the controller
EloquentLaravel’s ORM for interacting with the database
ArtisanLaravel’s CLI for generating code and managing the app
.envFile for storing environment-specific secrets

Written while learning Laravel on the job. References: Laravel 12.x official documentation.