Laravel Interview Questions · 2026

Laravel Interview Questions (2026): Most Asked, With Answers

A backend candidate at a mid-size logistics company got tripped up last quarter on a question that had nothing to do with syntax: why did adding a second `with()` relationship to an existing Eloquent query suddenly turn a page that loaded in 80ms into one that took four seconds. He'd shipped Laravel for two years and had never had to reason about what the query builder was actually generating at the implementation level, only about which chained method made the page work. Laravel sits at 9.3 percent usage among professional developers who work with PHP, itself used by 19.1 percent of professional developers overall, per the 2025 Stack Overflow Developer Survey. It's a smaller slice than Node or React, but the roles that use it, agencies, SaaS backends, internal tools, skew toward wanting someone who actually understands the framework rather than someone who copy-pasted a tutorial.

Here's the pattern worth naming: most Laravel interview prep spends its time on Artisan commands and folder structure because those are easy to quiz and easy to answer from memory. The service container, the N+1 query problem, and how middleware actually gets ordered are what separate a candidate who's built something real from one who's followed the docs example. You can ship a small CRUD app leaning entirely on the container's zero-configuration resolution without ever manually binding an interface. You can't debug a production slowdown, or write a package other developers will use, without understanding what the container is doing for you by default.

This page covers Laravel interview questions across eight areas: routing and the request lifecycle, the service container and service providers, middleware, Eloquent models and mass assignment, relationships and eager loading, validation and API resources, queues and events, and the harder edge cases around testing, auth, and race conditions. Laravel 13, current as of this writing, requires PHP 8.3 or newer and shipped in March 2026 (Laravel 13.x release notes), so version-specific answers below assume that baseline.

50Questions
9.3%Laravel Usage (Pro Devs, 2025)
8.3Laravel 13 Min PHP
March 2026Laravel 13 Release

Routing, Artisan, and the request lifecycle

Warm-up territory, but a candidate who fumbles the basics here rarely recovers the interviewer's confidence later.

Easy questions

14

The request enters through public/index.php, which bootstraps the application and hands the request to the HTTP kernel. The kernel runs the request through the global middleware stack, then the router matches it against your route definitions, applying any route-specific or group middleware along the way. The matched route or controller method runs, usually returning a response object, and that response travels back out through the same middleware stack (in reverse, for anything with post-processing logic) before it's sent to the browser.

Interviewers ask this to check whether you think of middleware as a single gate versus a series of layers a request has to pass through both ways.

Both are route files, but they get different default middleware groups applied automatically. Routes in web.php get the web group: session handling, CSRF protection, cookie encryption, so they work for traditional server-rendered pages and forms. Routes in api.php get the api group, which is much lighter, no sessions or CSRF by default, since API clients are expected to authenticate with tokens rather than cookies.

Artisan is Laravel's command-line tool for scaffolding, running migrations, clearing caches, and a long list of framework tasks you'd otherwise have to write by hand. make:model, make:controller, and make:migration handle boilerplate. migrate and db:seed manage the database. route:list is genuinely useful for debugging a routing conflict, and tinker drops you into a REPL with your whole application booted, which is faster than writing a throwaway test route just to poke at a query.

Route::resource registers all seven RESTful actions, index, create, store, show, edit, update, destroy, matching a typical web CRUD flow including the create and edit forms. Route::apiResource registers only five, dropping create and edit, since an API client doesn't need a route that renders an HTML form.

The service container is Laravel's tool for managing class dependencies and resolving them automatically, so instead of a class constructing its own dependencies with new, it type-hints what it needs and the container builds and injects them (Laravel service container docs). This matters because it decouples a class from the concrete implementation of what it depends on, which makes swapping a real service for a mock in tests trivial, and lets you change an implementation application-wide by changing one binding instead of every call site.

Middleware inspects and filters HTTP requests before they reach your application, or the response before it goes back out (Laravel middleware docs). Think of it as a stack of layers a request passes through, each one able to reject the request outright or let it continue by calling $next($request).

php
public function handle(Request $request, Closure $next): Response
{
  if ($request->input('token') !== 'secret') {
    return redirect('/home');
  }
  return $next($request);
}

A role-check middleware is the standard case: the middleware itself is generic, but the required role varies per route.

php
public function handle(Request $request, Closure $next, string $role): Response
{
  if (! $request->user()->hasRole($role)) {
    abort(403);
  }
  return $next($request);
}
// Route::put('/post/{id}',...)->middleware('role:editor');

Eloquent is Laravel's ActiveRecord-style ORM, where each model class maps to a database table and represents a row as an object with typed-feeling attribute access. By convention, a model called Post maps to a table called posts, plural snake_case of the class name, unless you override it with a $table property.

casts converts a raw database value into a more useful PHP type automatically. Common ones: 'is_active' => 'boolean' for tinyint columns that should behave like booleans, 'metadata' => 'array' or 'collection' for a JSON column, 'published_at' => 'datetime' for a Carbon instance instead of a raw string, and 'status' => Status::class for casting straight to a native PHP enum.

save() persists a model instance you already have, whether it's new or being updated. create() is a one-line shortcut: instantiate, mass assign, and save in one call. firstOrCreate() looks for a matching row first and only creates one if nothing matched, useful for avoiding duplicate rows in things like tag or category tables where you don't want a second identical entry.

By convention Eloquent guesses the foreign key from the related model's name: a Post hasMany Comment relationship assumes the comments table has a post_id column. belongsTo is the inverse side of that same relationship, sitting on the Comment model and looking for the same post_id. You override the convention with explicit arguments, hasMany(Comment::class, 'article_id'), whenever the column doesn't follow the default naming.

Looping over a collection and accessing a relationship on each item triggers a separate query per item, since by default Eloquent lazy loads: one query to fetch the N records, then N more to fetch each one's relationship, N+1 total. Fetching 25 books and reading $book->author->name in a loop runs 26 queries instead of 2 (Laravel Eloquent relationships docs). Eager loading with with('author') loads all the authors in a single second query up front, so the loop itself triggers zero additional queries.

php
$books = Book::with('author')->get(); // 2 queries total, not 26

Both are valid Laravel APIs, but a dedicated form request (php artisan make:request StorePostRequest) is the better default once a controller method has more than a handful of rules, or once the same validation needs to be reused across more than one action. It keeps the controller focused on orchestration instead of a wall of validation rules, and it gives you a natural place to put authorization logic too.

Anything slow and non-essential to the immediate response, sending an email, generating a PDF, calling a third-party API, blocks the user from getting their response back if it runs inline. Queuing it means the request returns immediately after dispatching the job, and a separate queue worker process picks it up and runs it in the background, which is the difference between a checkout page that takes 200ms and one that takes four seconds waiting on an email provider.

Medium questions

26

Route model binding lets Laravel automatically resolve a model instance from a route parameter instead of you manually querying for it inside the controller. Implicit binding just matches a type-hinted parameter name to a route segment, Route::get('/posts/{post}',...) with a type-hinted Post $post argument, and Laravel runs the lookup for you, throwing a 404 if nothing matches.

php
Route::get('/posts/{post}', function (Post $post) {
  return $post->title;
});

Explicit binding is when you register the resolution logic yourself, usually in a service provider, for cases where the default "match the column called id" behavior isn't what you want, resolving by a slug column instead, for example.

Because Laravel caches configuration into a single file when you run config:cache, and once that cache exists, the .env file is never read again at runtime. Any env() call outside a config file will return null in production after caching, since it's bypassing the cached config array entirely. The correct pattern is reading the value once in a config file, config('services.stripe.key'), and pulling it from config everywhere else in the app.

bind resolves a fresh instance every single time the container builds that class. singleton resolves once and returns the same instance for the rest of the request lifecycle, and for anything after that too, since it's application-scoped. scoped sits in between: one instance per request or job, but it gets flushed when a Laravel Octane worker starts a new request cycle, which matters because a plain singleton would otherwise leak state between requests under Octane.

php
$this->app->bind(Transistor::class, fn ($app) => new Transistor($app->make(PodcastParser::class)));
$this->app->singleton(Transistor::class, fn ($app) => new Transistor(...));
$this->app->scoped(Transistor::class, fn ($app) => new Transistor(...));

The container's zero-configuration resolution handles classes with no dependencies, or dependencies on other concrete classes, without any setup, since it can build the object graph with reflection alone. You need an explicit binding in two real situations: when a class depends on an interface rather than a concrete class, since the container has no way to guess which implementation you want, and when you're writing a package meant for other Laravel apps and need to register its services yourself.

register is where you should only bind things into the container, nothing else. boot runs after every provider's register method has already fired, so it's the safe place to use bindings that might come from another provider, register event listeners, or publish config files. Doing anything besides simple bindings inside register risks depending on a service another provider hasn't registered yet.

Contextual binding lets you inject a different implementation of the same interface depending on which class is asking for it. The classic example: two controllers both depend on a Filesystem contract, but one should get a local disk and the other an S3 disk.

php
$this->app->when(VideoController::class)
  ->needs(Filesystem::class)
  ->give(fn () => Storage::disk('s3'));

Global middleware runs on every request to the application, registered in bootstrap/app.php. Route middleware only runs on routes it's explicitly attached to. Middleware groups bundle several middleware under one name, web and api being the built-in examples, so a route can pull in a whole set (session handling, CSRF, cookie encryption for web) with one alias instead of listing each middleware individually.

Terminable middleware runs code after the response has already been sent to the browser, by defining a terminate method alongside handle. It's for work the user shouldn't have to wait on, logging request metrics, flushing queued session data, that doesn't need to block the response. Under FastCGI, PHP-FPM sends the response and then keeps executing, which is exactly the mechanism that makes this fast rather than a fake speed-up that secretly delays the response.

Mass assignment is when you pass a whole array of user input straight into create() or fill(), and if that array contains a field the user shouldn't control, an is_admin flag, say, submitted as extra form data, it gets silently written. $fillable is an allowlist: only listed attributes can be mass assigned. $guarded is the inverse, a blocklist of attributes that can't be. Most teams prefer $fillable because forgetting to add a new sensitive column to $guarded is a much easier mistake to make than forgetting to add a safe column to $fillable.

An accessor transforms a value when it's read from the model, a mutator transforms it before it's saved. Since Laravel 9, both live in one method returning an Attribute instance instead of two separate get*Attribute/set*Attribute methods.

php
protected function name(): Attribute
{
  return Attribute::make(
    get: fn (string $value) => ucfirst($value),
    set: fn (string $value) => strtolower($value),
  );
}

Adding the SoftDeletes trait to a model doesn't delete a row at all when you call delete(), it sets a deleted_at timestamp column instead. Every subsequent query automatically excludes rows with a non-null deleted_at by adding a global scope, so a soft-deleted row behaves as if it's gone for normal queries. You explicitly opt into seeing it with withTrashed() or restore it with restore(), and forceDelete() is what actually removes the row.

A many-to-many relationship needs an intermediate pivot table holding the two foreign keys, role_user for a User/Role relationship, following an alphabetical singular naming convention by default. belongsToMany on both models defines the relationship, and you can pull extra pivot columns (like a timestamp for when a role was assigned) with withPivot(), or automatically maintain created_at/updated_at on the pivot itself with withTimestamps().

php
public function roles(): BelongsToMany
{
  return $this->belongsToMany(Role::class)->withTimestamps();
}

with() is eager loading declared up front, as part of the original query, before you have any model instances yet. load() is for when you already have a model or collection in hand and realize afterward that you need a relationship, so you eager load it after the fact in one extra query instead of falling back into lazy loading inside a loop.

php
$book = Book::first();
$book->load('genres'); // one query, not per-access lazy loading

Call Model::preventLazyLoading() in your AppServiceProvider's boot method, gated to local and testing environments, so any lazy-loaded relationship throws an exception in development instead of quietly firing extra queries. That turns an N+1 bug from something you discover in a slow production dashboard into something that fails a local test run immediately. Laravel Telescope or a simple query-count assertion in a feature test catches the same class of bug if you'd rather not enforce it that strictly everywhere.

Comments that can belong to either a Post or a Video is the standard example: rather than a separate comments table per parent type, one comments table holds a commentable_id and commentable_type column, and morphTo()/morphMany() resolve the right parent model regardless of which type it points to. Relation::enforceMorphMap() is worth mentioning too, since it lets you store a short alias like "post" in commentable_type instead of the full class name, which keeps the database from being coupled to your app's namespace structure.

authorize() answers whether this user is allowed to make this request at all, returning false triggers a 403 before validation even runs. rules() answers whether the data itself is well-formed, required fields present, correct types, unique constraints satisfied. Mixing the two, putting an ownership check inside a validation rule, works but makes the failure harder to reason about, since a 422 validation error and a 403 authorization error should mean different things to the client.

An API resource is a transformation layer between an Eloquent model and the JSON you actually want to expose, so you're not accidentally serializing internal columns, password hashes, soft-delete timestamps, straight from toArray(). It also gives you one place to reshape or rename fields, add computed values, and conditionally include relationships, instead of scattering that logic across every controller that happens to return a Post.

php
class PostResource extends JsonResource
{
  public function toArray($request): array
  {
    return [
      'id' => $this->id,
      'title' => $this->title,
      'author' => new UserResource($this->whenLoaded('author')),
    ];
  }
}

Once the logic needs more than a couple of chained rules to express, or needs to query the database in a way the built-in rules don't cover, a rule verifying a promo code is both unvexpired and hasn't already been redeemed by this specific user, for instance. A custom rule class also gets you a reusable, testable unit instead of the same closure duplicated across every form request that needs it.

throttle:60,1 attaches a rate limiter allowing 60 requests per minute, keyed by default on the authenticated user's ID if there is one, falling back to IP address for guests. Named rate limiters, defined with RateLimiter::for() in a service provider, let you customize the key and limit per situation, a stricter limit for unauthenticated login attempts than for an authenticated user browsing an API, for example.

A job class implements ShouldQueue and defines its work in a handle method, which the container resolves dependencies into just like a controller. Implementing that interface is what tells dispatch() to push the job onto a queue instead of running it immediately in the current request.

php
class ProcessPodcast implements ShouldQueue
{
  public function __construct(public Podcast $podcast) {}
  public function handle(AudioProcessor $processor): void
  {
    $processor->process($this->podcast);
  }
}
ProcessPodcast::dispatch($podcast);

After exhausting its configured retry attempts (set via tries or the #[Tries] attribute), a failed job gets recorded in the failed_jobs table along with the exception, and a failed event fires that you can hook into for alerting. php artisan queue:retry lets you re-run a specific failed job once you've fixed whatever caused it, and failed() as a method on the job class runs custom cleanup logic, notifying someone, rolling back a partial state change, right when the failure is recorded.

Events decouple the thing that happened from the code that reacts to it. Firing an OrderShipped event lets you attach a notification listener, an analytics listener, and an inventory-update listener independently, without the order-shipping code needing to know any of them exist. Add a fourth reaction later and you write a new listener, you don't touch the original shipping logic at all, which is the actual payoff over a growing pile of direct method calls in one place.

Listening for Post::created(function ($post) {...}) directly works fine for one or two quick hooks, usually registered in a service provider's boot method. An Observer class is the better choice once a model has several lifecycle hooks worth reacting to, creating, updating, deleting, since it groups all of that logic into one dedicated class with a method per event instead of scattering closures across a provider.

php
class PostObserver
{
  public function created(Post $post): void { Cache::forget('posts.count'); }
  public function deleted(Post $post): void { Cache::forget('posts.count'); }
}

RefreshDatabase migrates the schema fresh (or resets it via a cached snapshot) before the test suite runs, then wraps each test in a transaction that gets rolled back afterward, so tests don't leak data into each other. DatabaseTransactions skips the migration step entirely and just wraps each test in a transaction, assuming the schema already exists and is correct, which is faster for a suite that isn't changing schema between runs.

Sanctum handles the two most common auth needs, SPA cookie-based authentication and simple API tokens, with far less setup than a full OAuth2 implementation. Passport is a complete OAuth2 server, which you actually need when you're issuing access to third-party applications you don't control, a public API with a proper authorization-code flow and scoped permissions, not just your own first-party SPA or mobile app talking to your own backend.

CSRF protection defends against a browser automatically attaching a logged-in user's cookies to a request from a malicious third-party site. It's a cookie-session problem specifically. API routes authenticated with a bearer token in an Authorization header aren't vulnerable to that same attack, since a malicious site can't read or attach a token it never had access to the way it can silently ride along on cookies. That's why the api middleware group skips the PreventRequestForgery middleware that web routes include by default.

Hard questions

10

Not really, and this is a common misconception worth correcting in an interview if it comes up. A facade is a static-looking proxy to an object resolved from the container, but Laravel's facades support swapping in a mock at test time through methods like Cache::shouldReceive() or Http::fake(), which intercept the underlying resolved instance rather than the static call itself. The real testability argument against facades isn't that they can't be mocked, it's that overusing them scatters implicit dependencies through a class instead of making them visible in the constructor, which hurts readability more than testability.

The classic case is a rate-limiting or auth middleware running before session or cookie middleware has set things up, so it ends up reading stale or missing state. Laravel exposes a priority method in bootstrap/app.php for exactly this, letting you pin a fixed execution order (session before authentication, authentication before authorization) regardless of the order individual routes attach middleware in. I've seen a throttle middleware silently rate-limit against the wrong key because it ran before the auth middleware had resolved the current user, so it fell back to IP-based limiting for everyone behind the same NAT.

When the transformation between the database representation and the PHP representation is genuinely two-way and non-trivial, encrypting a value at rest and decrypting on read, or converting a stored JSON blob of coordinates into a dedicated value object with its own methods. You implement CastsAttributes with get and set methods, which keeps that logic in one reusable place instead of duplicating encrypt/decrypt calls at every place the model is touched.

php
class Money implements CastsAttributes
{
  public function get($model, $key, $value, $attributes) {
    return new MoneyValue($value / 100);
  }
  public function set($model, $key, $value, $attributes) {
    return $value->cents();
  }
}

A local scope is a reusable query constraint defined as a method prefixed with scope, called without that prefix wherever you build a query, Post::published()->get() for a scopePublished method. A global scope applies automatically to every query against that model without being called explicitly, the way SoftDeletes silently filters out trashed rows on every query. The tradeoff: local scopes are explicit and easy to reason about at the call site, global scopes are convenient but can surprise a teammate who doesn't know one is silently filtering their query, which is exactly the complaint people have about soft deletes hiding rows unexpectedly.

request->all() returns every input the client sent, including fields that were never declared in the validation rules at all. validated(), called after validation has run, returns only the fields that were actually validated, which means it's safe to pass straight into create() or update() without accidentally mass-assigning something the client snuck into the payload that wasn't covered by a rule. It's a subtle but real difference from the mass-assignment vulnerability question earlier, since $fillable protects against unexpected columns and validated() protects against unexpected keys reaching that point at all.

It prevents the same logical job from being queued more than once while an existing instance of it is still pending or running, keyed by a value you define with uniqueId(). This matters for something like a report-generation job triggered by a button a user might double-click, or a webhook that occasionally fires twice for the same event, since without it you'd process the same expensive work redundantly or, worse, race two instances writing to the same row at once.

php
class GenerateReport implements ShouldQueue, ShouldBeUnique
{
  public function __construct(public int $reportId) {}
  public function uniqueId(): string { return (string) $this->reportId; }
}

Batching groups a set of related jobs so you can track overall progress, run a single callback once every job in the set finishes, and cancel the remaining jobs in the batch if one of them fails. Reprocessing a thousand rows of an imported CSV is the standard case: without batching you'd have no clean way to know when "the import" as a whole is actually done, or to stop queuing more work once you've decided the import failed partway through.

php
Bus::batch($jobs)->then(function (Batch $batch) {
  // all jobs completed successfully
})->catch(function (Batch $batch, Throwable $e) {
  // first failure
})->dispatch();

DB::transaction() wraps a closure so every query inside it either all commits or all rolls back if any exception is thrown, which matters for anything that touches more than one table and needs to stay consistent, transferring a balance between two accounts, for instance. Deadlocks happen when two transactions each hold a lock the other is waiting for, and MySQL will kill one of them with a deadlock exception rather than let both hang forever. DB::transaction($closure, $attempts) accepts a retry count specifically for this, automatically re-running the whole closure from scratch if it catches a deadlock, since retrying is usually safe and successful the second time once the competing lock has cleared.

Without a unique constraint at the database level, both requests can run their "does this exist" check, both see nothing, and both insert, giving you a duplicate row despite the method's name implying it can't happen. firstOrCreate is not atomic by itself, it's a select followed by an insert, and there's a real window between those two steps under real concurrency. The fix is a unique index on the relevant columns and catching the resulting integrity-constraint exception on the losing request, or wrapping the whole thing in a transaction with a row lock, not just trusting the method name.

Octane keeps your application booted in memory across requests using a long-running worker (Swoole or RoadRunner) instead of the normal bootstrap-per-request PHP model, which is much faster but means any state you'd normally assume resets between requests doesn't automatically. Static properties, singletons holding request-specific data, and anything cached in a class property will leak from one request into the next unless you're deliberate about resetting it, which is exactly what the scoped container binding and Octane's request-lifecycle hooks exist to manage. Code that "just works" under the normal per-request model can silently misbehave the first time it runs under Octane if it was quietly relying on that reset happening for free.

How to prepare for a Laravel interview

Skip another pass through the Artisan command list. Build one small thing that forces you to hit the parts of Laravel that don't show up in a CRUD tutorial: a job that has to be unique and retried safely, an Eloquent query you deliberately break with N+1 and then fix with eager loading while watching the query count, a form request with a custom validation rule that hits the database. Turn on Model::preventLazyLoading() locally and fix every exception it throws instead of ignoring it. Watching your own query count drop from 26 to 2 teaches eager loading faster than reading the docs section on it ever will.

Across backend and full-stack mock interviews run through LastRoundAI, the N+1 question and the "what does validated() protect against that fillable doesn't" question trip up more mid-level candidates than the service container questions do, even though container internals get more attention in prep guides. My read is that generics-style container trivia feels like the "advanced" topic worth studying on purpose, while query performance feels like something you'll just pick up on the job. We don't track a precise percentage on that gap, but it comes up often enough in review to flag here.

One more thing worth knowing walking into a 2026 interview: Laravel 13 requires PHP 8.3 as a minimum and ships a first-party AI SDK plus native vector/embedding support for building semantic search directly against Postgres and pgvector (Laravel 13.x release notes). It's a maintenance-style release with minimal breaking changes, so an interviewer probing your Laravel 13 knowledge is more likely asking whether you've noticed the new attribute-based middleware and authorization syntax than whether you've memorized a migration guide.

Get the reps in before the real thing

Explaining eager loading on a whiteboard is not the same as defending it out loud once an interviewer changes one relationship and asks what happens to your query count. LastRoundAI's mock interview mode runs live coding rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19/mo if a handful of sessions isn't enough runway.

Once your answers hold up under a follow-up, the slower part of the job hunt is usually just getting in front of enough backend and full-stack roles that actually test Laravel instead of treating it as a resume keyword. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

Should I memorise Laravel syntax for the interview?

Rarely worth it. Most interviewers care that you know what to reach for and why, and will not fail you for forgetting an exact flag. Being confidently wrong about behaviour costs far more than admitting you would check the documentation.

What is the most common mistake in Laravel interviews?

Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.

How long does it take to prepare for a Laravel interview?

If you already work with Laravel day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.

What Laravel topics come up most often?

Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.

Leave a Reply

Your email address will not be published. Required fields are marked *