<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: K. Polash</title>
    <description>The latest articles on DEV Community by K. Polash (@polash).</description>
    <link>https://dev.to/polash</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1241000%2F8c8504ee-e938-418e-b885-2111bea6aee4.jpg</url>
      <title>DEV Community: K. Polash</title>
      <link>https://dev.to/polash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/polash"/>
    <language>en</language>
    <item>
      <title>Building Vendzoo: How I Built a Full Business OS for SMEs — Fraud Detection, 4 Couriers, RFM Engine &amp; More</title>
      <dc:creator>K. Polash</dc:creator>
      <pubDate>Sat, 15 Aug 2026 09:31:54 +0000</pubDate>
      <link>https://dev.to/polash/building-vendzoo-how-i-built-a-full-business-os-for-smes-fraud-detection-4-couriers-rfm-engine-2094</link>
      <guid>https://dev.to/polash/building-vendzoo-how-i-built-a-full-business-os-for-smes-fraud-detection-4-couriers-rfm-engine-2094</guid>
      <description>&lt;p&gt;&lt;em&gt;From COD fraud nightmares to automated intelligence: the story of building a business platform for Bangladesh's e-commerce market.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Problem That Started Everything
&lt;/h2&gt;

&lt;p&gt;Picture this.&lt;/p&gt;

&lt;p&gt;A small shop owner is managing their online business. They've got WooCommerce for the website, Excel sheets for stock tracking, Pathao open on one phone, Steadfast on another, and Facebook Page orders coming in through DMs. They have a physical notebook for customer history, and absolutely &lt;strong&gt;no way&lt;/strong&gt; to know if a new customer is a fraudster who'll refuse the delivery.&lt;/p&gt;

&lt;p&gt;Every morning starts with copy-pasting order details from three different places. Every afternoon is spent manually messaging courier agents. Every evening is reconciling which orders got delivered, which got returned, and how much money actually came in.&lt;/p&gt;

&lt;p&gt;This isn't a unique story. This is the &lt;strong&gt;daily reality&lt;/strong&gt; of thousands of SME owners, retailers, and e-commerce merchants.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Vendzoo&lt;/strong&gt; to end this chaos.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Vendzoo&lt;/strong&gt; is an all-in-one SaaS Business OS: POS, Inventory, Courier, Fraud Detection, Customer Intelligence, Marketing, and Analytics, all in one dashboard.&lt;br&gt;
🌐 &lt;a href="https://vendzoo.com" rel="noopener noreferrer"&gt;vendzoo.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the story of how it was built, the real problems we solved, and the decisions that shaped the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ The System at a Glance
&lt;/h2&gt;

&lt;p&gt;Vendzoo is built on &lt;strong&gt;Laravel 13 with PHP 8.3&lt;/strong&gt;, backed by MySQL, with a Tailwind CSS v4 and Vite 8 frontend. Nothing exotic, just a solid, modern stack chosen for reliability and developer ergonomics.&lt;/p&gt;

&lt;p&gt;What makes it interesting isn't the stack. It's the three layers sitting on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core layer&lt;/strong&gt; handles POS, orders, inventory, invoicing, and multi-user access with role-based permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration layer&lt;/strong&gt; connects to everything a merchant already uses: WooCommerce, Shopify, Facebook Commerce, Pathao, Steadfast, RedX, Carrybee, Firebase, Telegram, SMS, WhatsApp, and Email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The intelligence layer&lt;/strong&gt; is where Vendzoo earns its "Business OS" label: a fraud risk engine, customer segmentation, churn prediction, courier performance analytics, inventory velocity tracking, and full profit &amp;amp; loss reporting. All running automatically in the background.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1: The Courier Problem: 4 Companies, 1 Unified System
&lt;/h2&gt;

&lt;p&gt;Bangladesh's e-commerce runs almost entirely on &lt;strong&gt;COD (Cash on Delivery)&lt;/strong&gt;. And there are four major courier companies: Pathao, Steadfast, RedX, and Carrybee. Each has its own API, its own authentication system, its own webhook format, and its own quirks.&lt;/p&gt;

&lt;p&gt;The tempting approach is to write four separate integrations and treat each courier differently throughout the codebase. We didn't do that.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Contract, Four Implementations
&lt;/h3&gt;

&lt;p&gt;Every courier in Vendzoo follows the exact same internal contract. Whether the merchant is using Pathao or Carrybee, the rest of the system asks the same question in the same way and gets the same type of answer back. The dashboard doesn't know or care which courier it's talking to.&lt;/p&gt;

&lt;p&gt;This means adding a new courier in the future requires writing exactly one new implementation: nothing else in the system needs to change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Delivery Pricing Modes, One Calculator
&lt;/h3&gt;

&lt;p&gt;Different merchants price delivery differently. Some charge a flat rate: ৳60 inside Dhaka, ৳120 outside. Some use weight-based tiers with different prices for different brackets. Some let the courier's own API calculate the charge live based on distance and weight.&lt;/p&gt;

&lt;p&gt;A single delivery calculator handles all three modes, knows which to use, and falls back gracefully if the courier API is temporarily unavailable. The merchant configures their preference once. After that, pricing just works, in POS orders, WooCommerce orders, and manual orders alike.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Webhook Lifecycle
&lt;/h3&gt;

&lt;p&gt;When Pathao delivers an order, or returns it, they notify Vendzoo via webhook. The flow is immediate: the webhook arrives, we identify the courier, we update the order status, we log the update, and we notify the merchant via push notification and Telegram, all within seconds.&lt;/p&gt;

&lt;p&gt;We keep each courier's updates in dedicated storage rather than one mixed log. This makes reporting cleaner and debugging faster. If something's off with Steadfast tracking, you look at Steadfast's data, not a pile of mixed records from all four couriers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2: The Fraud Engine: Protecting Merchants from COD Return Losses
&lt;/h2&gt;

&lt;p&gt;This is the feature I'm proudest of.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why COD Fraud Hurts More Than People Realize
&lt;/h3&gt;

&lt;p&gt;When a customer refuses a COD delivery, the merchant doesn't just miss the sale. They absorb the outbound delivery fee, the return delivery fee, the COD handling charge, and sometimes the cost of damaged packaging, on top of the lost inventory value. Do this a few hundred times a month, and it becomes a serious financial problem.&lt;/p&gt;

&lt;p&gt;The frustrating part is that these risky customers are often repeat offenders. The same phone number that refused delivery on three other merchants' orders will show up as a fresh "new customer" on your store, with no visible history.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Three-Pillar Risk Score
&lt;/h3&gt;

&lt;p&gt;Every order in Vendzoo receives a &lt;strong&gt;Unified Risk Score from 0 to 100&lt;/strong&gt;, built from three independent signals that we combine with different weightings.&lt;/p&gt;

&lt;p&gt;The first signal is the customer's delivery success rate within your own store. If they've placed multiple orders with you and consistently accepted delivery, that's a strong positive signal. If they've returned or refused most of their orders, that weight pulls the score down.&lt;/p&gt;

&lt;p&gt;The second signal comes from an external courier fraud database that tracks delivery behavior across many merchants and platforms, not just yours. A customer who is completely new to your store might have a well-documented history of refusing deliveries elsewhere. This cross-platform view is what makes the score meaningfully different from just checking your own records.&lt;/p&gt;

&lt;p&gt;The third signal is a basic consistency check: does the phone number on the customer's profile match the phone number on the shipping address? A mismatch is a small red flag. Fraud attempts often involve different contact numbers at different stages.&lt;/p&gt;

&lt;p&gt;A combined score above 80 is low risk. The 50–80 range is medium, proceed with awareness. Below 50 triggers an instant alert to the merchant before the parcel is dispatched.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happens When the External Service Goes Down?
&lt;/h3&gt;

&lt;p&gt;This was one of the most important design decisions in the entire system. External APIs go down. Servers have outages. Networks have timeouts.&lt;/p&gt;

&lt;p&gt;If the fraud data service is unavailable, Vendzoo doesn't block orders or throw errors. It falls back in layers: first to recent cached data for that phone number, then to older cached data marked as stale, and finally to a neutral mid-range score if nothing is cached at all. The merchant always gets a result. The order always flows. The system is never hostage to a third-party outage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing Duplicate Alerts Under Load
&lt;/h3&gt;

&lt;p&gt;Here's a subtle problem: if two background workers pick up the same high-risk order at the same moment, the merchant could receive two identical "HIGH RISK!" alerts. Confusing and unprofessional.&lt;/p&gt;

&lt;p&gt;The fix is an atomic database update. The first worker to record the notification wins. The second worker checks, sees it's already been recorded, and exits quietly. No matter how many workers are running in parallel, exactly one notification goes out, guaranteed at the database level, not just in application logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3: Customer Intelligence: Knowing Who Your Customers Really Are
&lt;/h2&gt;

&lt;p&gt;Every merchant has the same questions, but almost none of them have good answers: Who are my most loyal customers? Who bought once and never came back? Who is about to churn? Who should I send a win-back offer to right now?&lt;/p&gt;

&lt;p&gt;Vendzoo builds answers to all of these automatically, without the merchant lifting a finger.&lt;/p&gt;

&lt;h3&gt;
  
  
  RFM: A Framework That's Stood the Test of Time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;RFM&lt;/strong&gt; stands for &lt;strong&gt;Recency, Frequency, and Monetary value&lt;/strong&gt;. It's a customer segmentation method that's been used in direct marketing for decades, and it remains one of the most practical and interpretable frameworks for understanding customer behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recency&lt;/strong&gt; measures how recently a customer placed their last order. A customer who ordered yesterday is more engaged than one who ordered eight months ago, even if the older customer's total spend is higher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frequency&lt;/strong&gt; counts how many successful deliveries a customer has completed. Repeat buyers who consistently accept delivery are a fundamentally different category from one-time buyers, even if the one-time buyer spent more on that single order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monetary&lt;/strong&gt; tracks the total revenue generated from completed orders. This filters out the customers who order often but return often too; only successfully delivered orders contribute to the monetary score.&lt;/p&gt;

&lt;p&gt;Each dimension gets a score, and the combined result automatically places every customer into a persona: Champions, Loyal Customers, At Risk, Big Spenders, Promising, Hibernating, High Return Risk, or Lost. These labels update in the background continuously, so when a merchant opens their customer list, they immediately see who needs attention and who deserves a reward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Churn Prediction Without Machine Learning
&lt;/h3&gt;

&lt;p&gt;Separately from the RFM segmentation, Vendzoo runs a lightweight churn prediction based on a simple principle: the longer since a customer's last order, the higher the probability they've moved on.&lt;/p&gt;

&lt;p&gt;Customers who haven't ordered in over a month get flagged as potentially at risk. Those who've gone quiet for 60 days are hibernating. At 90 days, they're classified as lost. These aren't predictions from a trained model; they're clear, explainable thresholds that work well for the SME context and can be acted on directly without a data scientist in the loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 4: Inventory Intelligence: Smarter Stock Management
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sales Velocity
&lt;/h3&gt;

&lt;p&gt;Vendzoo tracks the average number of units sold per day for every product, what's commonly called &lt;strong&gt;sales velocity&lt;/strong&gt;. It's derived from historical sales movement data, calculated automatically, and updated daily.&lt;/p&gt;

&lt;p&gt;The value of knowing velocity is simple: a product selling 15 units a day has completely different restocking needs than one selling 3 units a week. Without this number, merchants either reorder too late (stockouts) or too early (cash tied up in dead inventory).&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Reorder: Suggested Automatically
&lt;/h3&gt;

&lt;p&gt;Using sales velocity together with the typical lead time to receive new stock, Vendzoo suggests a reorder threshold for each product. When current stock drops to that threshold, it's time to reorder, not when stock hits zero. The buffer accounts for the days it takes new stock to arrive and a small safety cushion for demand spikes.&lt;/p&gt;

&lt;p&gt;These suggestions appear on the inventory dashboard without any configuration from the merchant. The system calculates them fresh in the background every day, invisibly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built for Scale from the Start
&lt;/h3&gt;

&lt;p&gt;One early performance mistake was calculating velocity for each product one at a time: a separate database query per SKU. With 10 products in development, this was fine. In production with a merchant managing 2,000 SKUs, it became a serious bottleneck.&lt;/p&gt;

&lt;p&gt;The fix was restructuring the calculation to aggregate data for all products in a single database query, then distribute the results. The math is identical; the number of queries dropped from 2,000 to 1. This now runs entirely in the background, finishing long after the merchant's page has loaded, with zero impact on their experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 5: Courier Intelligence: Which Courier Actually Performs Best for You?
&lt;/h2&gt;

&lt;p&gt;Every merchant has a preferred courier. Most of those preferences are based on gut feel, past complaints, or a colleague's recommendation, not data.&lt;/p&gt;

&lt;p&gt;Vendzoo builds the data view instead. For every courier a merchant has ever used, it calculates the actual delivery success rate broken down by destination area, using the merchant's own real order history, not industry averages or courier marketing materials.&lt;/p&gt;

&lt;p&gt;If a merchant has shipped 200 orders to Chittagong with RedX and 180 were delivered, that's a 90% success rate for that courier in that area. If Pathao handled 150 orders to the same area with 105 deliveries, that's 70%. The recommendation is clear, and it's built entirely from the merchant's own data, no external API required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 6: WooCommerce &amp;amp; Shopify: One Dashboard, All Channels
&lt;/h2&gt;

&lt;p&gt;Many Bangladesh merchants have both an online store and a physical shop. The inventory nightmare: the website shows 10 units in stock, but 8 were already sold at the counter this morning.&lt;/p&gt;

&lt;p&gt;Vendzoo solves this by treating all channels as part of the same inventory pool. When an order is placed on WooCommerce, stock is deducted from the same pool that POS orders use. When a Shopify order syncs, the same thing. There's one source of truth, and every channel reads from and writes to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  When a WooCommerce Order Arrives
&lt;/h3&gt;

&lt;p&gt;A customer places an order on the website. Within seconds, the webhook reaches Vendzoo: we check for duplicates, find or create the customer by phone number, resolve the delivery charge, create the order in the Vendzoo dashboard, deduct stock, and notify the merchant. From that point on, the merchant handles everything in Vendzoo: booking the courier, updating status, managing returns. The WooCommerce dashboard becomes unnecessary for day-to-day operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shopify at Scale
&lt;/h3&gt;

&lt;p&gt;Syncing inventory to Shopify for a merchant with thousands of products can't be done in one shot: it'll time out. Vendzoo breaks the sync into small batches and processes each batch as a separate background job. If one batch fails due to a network issue or API rate limit, only that batch retries, not the entire catalog.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 7: Marketing Automation: Targeted, Not Broadcast
&lt;/h2&gt;

&lt;p&gt;Most merchant marketing is the same blast to everyone: "20% off this weekend!" Vendzoo makes it possible to do something much more effective: send the right message to the right customer at the right moment.&lt;/p&gt;

&lt;p&gt;Because every customer already has an RFM badge, merchants can build campaigns that target specific segments. A win-back campaign can go exclusively to customers who haven't ordered in over 45 days. A loyalty reward can target Champions and Loyal Customers only. High Return Risk customers can be excluded from all outgoing campaigns automatically.&lt;/p&gt;

&lt;p&gt;The audience builds itself. The merchant writes the message and picks the segment.&lt;/p&gt;

&lt;p&gt;Behind the scenes, campaigns use a dedicated queue channel, enforce rate limits per messaging provider, and retry failed sends with increasing delays: 30 seconds, then 2 minutes, then 5 minutes. Campaign sending also pauses automatically after 10 pm to respect customer boundaries, and resumes the next morning.&lt;/p&gt;

&lt;p&gt;If a merchant manually triggers a campaign at the exact same moment a scheduled send fires, the system ensures only one dispatch happens, through an atomic lock at the database level that prevents any double-sending.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 8: Real-Time Notifications
&lt;/h2&gt;

&lt;p&gt;Merchants need to know about new orders and delivery updates the instant they happen, not on their next login.&lt;/p&gt;

&lt;p&gt;Vendzoo supports push notifications through Firebase and message delivery through Telegram. Connecting Telegram is a one-tap flow: the merchant gets a unique time-limited link, taps it, and their Telegram account is permanently linked. Every important event (new order, parcel delivered, high-risk alert) arrives as a Telegram message in addition to a push notification.&lt;/p&gt;

&lt;p&gt;The Firebase integration is built without any heavyweight SDK dependency: a lightweight custom implementation that handles token caching and minimizes unnecessary API calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 9: Order Lifecycle: A State Machine That Prevents Mistakes
&lt;/h2&gt;

&lt;p&gt;Orders in Vendzoo follow a defined lifecycle, and transitions between states are enforced by the system itself. You can't mark an order as returned without it having been delivered first. You can't skip from pending to completed in a delivery order. The only transitions the UI shows are the ones that are actually valid from the current state.&lt;/p&gt;

&lt;p&gt;For takeaway (POS) orders, the flow is simple: Pending to Completed, with Cancelled and On Hold as side paths.&lt;/p&gt;

&lt;p&gt;For delivery orders, the full path runs from Pending through Confirmed, Ready to Ship, In Courier, Delivered, and finally Completed, with Returned, On Hold, and Cancelled available at the right stages.&lt;/p&gt;

&lt;p&gt;The result is data that's always logically consistent. Reporting is clean. Status histories make sense. There's no way to accidentally create an order that's "delivered but was never with a courier."&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 10: Subscriptions: Feature Limits Done Right
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Locking In What You Signed Up For
&lt;/h3&gt;

&lt;p&gt;When a merchant subscribes to a plan, Vendzoo takes a snapshot of that plan's limits and locks them to the subscription. If an admin later changes the plan's limits for new subscribers, existing merchants are unaffected, they keep what they signed up for, for the duration of their subscription.&lt;/p&gt;

&lt;p&gt;This matters for trust. Merchants shouldn't wake up to find they've exceeded limits on a plan they've been paying for, because someone changed the rules without them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading Without Losing Your History
&lt;/h3&gt;

&lt;p&gt;When a merchant upgrades to a higher-tier plan, their existing usage counts carry over correctly. A merchant who had 90 products on their previous plan doesn't get a "free" reset to zero products used when they upgrade: the 90 transfers to the new plan's higher limit. Small detail, but it closes a loophole that would otherwise let merchants game usage tracking.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Vendzoo Has Become
&lt;/h2&gt;

&lt;p&gt;What started as an attempt to replace a few Excel sheets has grown into something much larger: 76 data models, 35 service classes, 31 background jobs, four courier integrations, three e-commerce platform connections, and five notification channels, all working together in a system that feels simple to use because the complexity is fully hidden.&lt;/p&gt;

&lt;p&gt;The stack is Laravel 13 with PHP 8.3, Tailwind CSS v4, Vite 8, Pest PHP 4 for testing, and Sentry for production monitoring. The development environment starts with a single command that concurrently runs the server, frontend hot-reload, queue worker, and log viewer in one terminal window.&lt;/p&gt;

&lt;p&gt;The product is live. Real merchants use it daily. And there's still a long roadmap ahead: a mobile app, bKash and Nagad payment gateway integration, automated COD remittance reconciliation, and multi-currency support for cross-border selling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try Vendzoo
&lt;/h2&gt;

&lt;p&gt;🌐 &lt;strong&gt;&lt;a href="https://vendzoo.com" rel="noopener noreferrer"&gt;vendzoo.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>Why Your System Fails on the Most Predictable Day of the Year</title>
      <dc:creator>K. Polash</dc:creator>
      <pubDate>Wed, 01 Apr 2026 16:30:00 +0000</pubDate>
      <link>https://dev.to/polash/why-your-system-fails-on-the-most-predictable-day-of-the-year-df1</link>
      <guid>https://dev.to/polash/why-your-system-fails-on-the-most-predictable-day-of-the-year-df1</guid>
      <description>&lt;p&gt;Most applications don't fail because of bad code.&lt;/p&gt;

&lt;p&gt;They fail because of bad architecture decisions made early that nobody questioned &lt;br&gt;
until it was too late.&lt;/p&gt;

&lt;p&gt;Here's what actually breaks systems at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everything talking to everything (no clear boundaries)&lt;/li&gt;
&lt;li&gt;The database doing work the application should do&lt;/li&gt;
&lt;li&gt;Synchronous processing where async was needed&lt;/li&gt;
&lt;li&gt;One giant service that owns too much responsibility&lt;/li&gt;
&lt;li&gt;No separation between reads and writes under heavy load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are framework problems. None of these are language problems.&lt;br&gt;
&lt;strong&gt;They are thinking problems.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scenario: University Enrollment Day
&lt;/h2&gt;

&lt;p&gt;Think about course enrollment day at any university.&lt;/p&gt;

&lt;p&gt;Every semester, thousands of students flood the portal at the exact same time, each trying to register for their courses. The load is not a surprise. &lt;strong&gt;The date is on the calendar.&lt;/strong&gt; It happens every single semester like clockwork.&lt;/p&gt;

&lt;p&gt;But the system was never designed for it.&lt;/p&gt;

&lt;p&gt;Every request hits the same flow — check eligibility, check seat availability, write enrollment, update seat count — all synchronously, all at once. No queue. No cache. No separation between reads and writes. Just a database choking under the weight of an entirely predictable moment.&lt;/p&gt;

&lt;p&gt;Students get timeout errors. Duplicate enrollments. Lost seats they were fully eligible for. Everyone refreshes in panic. And every semester, someone calls IT to "increase the server capacity" and nothing really changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The code isn't broken. The thinking is.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Actually Going On
&lt;/h2&gt;

&lt;p&gt;Most teams look at this and see one problem: too much traffic. So they throw more servers at it. It helps a little, then fails again next semester.&lt;/p&gt;

&lt;p&gt;The reality is there are &lt;strong&gt;5 separate problems&lt;/strong&gt; here, each requiring a different solution. Solving one without the others just moves the failure to a different place.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔥 The Spike
&lt;/h3&gt;

&lt;p&gt;Thousands of simultaneous requests will bring any database to its knees regardless of hardware. A queue, a cache layer, and read/write separation need to work together. Most teams implement one and call it done.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔁 The Race Condition
&lt;/h3&gt;

&lt;p&gt;Even with a queue, two workers can read "1 seat available" at the same time, both pass eligibility, and both enroll into the last seat. The queue serialized intake. It did not serialize processing. You need locking — pessimistic, optimistic, or distributed — and each has real tradeoffs.&lt;/p&gt;

&lt;h3&gt;
  
  
  👆 The Double Click
&lt;/h3&gt;

&lt;p&gt;A student hits Enroll and the page is slow. They click again. Now two identical requests are in flight. Even with locking in place, without idempotency handling both clicks can create two enrollment records. This is not a database problem. It is an &lt;strong&gt;API design problem&lt;/strong&gt; — and the one most teams discover only after finding duplicate records in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  💥 The Half-Enrolled Student
&lt;/h3&gt;

&lt;p&gt;A successful enrollment is not one database write. It is a chain: write the record, decrement the seat count, send confirmation, update the academic record, generate a fee entry. What happens if the system crashes after step 2?&lt;/p&gt;

&lt;p&gt;The seat is taken. The student has no confirmation. The database says enrolled. The academic record disagrees. &lt;strong&gt;Designing for the middle of failure is architecture.&lt;/strong&gt; Designing only for the happy path is wishful thinking.&lt;/p&gt;

&lt;h3&gt;
  
  
  🕳️ The Stale Cache Trap
&lt;/h3&gt;

&lt;p&gt;The cache says 5 seats available. The database already has 0. Students attempt to enroll based on stale data, hit the lock, fail, and get a confusing error even though the portal showed availability 3 seconds ago. The cache improved performance but silently introduced a &lt;strong&gt;trust problem&lt;/strong&gt;. Cache invalidation strategy needs to be designed upfront, not patched after support tickets pile up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Questions That Actually Matter
&lt;/h2&gt;

&lt;p&gt;The engineers who scale systems well ask different questions from the start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where are my bottlenecks under 10x load?&lt;/li&gt;
&lt;li&gt;What happens if this one service goes down?&lt;/li&gt;
&lt;li&gt;Am I coupling things that should be independent?&lt;/li&gt;
&lt;li&gt;What happens in the &lt;em&gt;middle&lt;/em&gt; of a failure, not just at the end?&lt;/li&gt;
&lt;li&gt;Can the same request safely arrive twice?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these have anything to do with which framework you picked or which cloud provider you use. They are design questions. Thinking questions.&lt;/p&gt;




&lt;p&gt;I wrote a full deep dive on all 5 problems with concrete solutions for each one.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.linkedin.com/pulse/why-your-system-fails-most-predictable-day-year-k-polash-q0mxc" rel="noopener noreferrer"&gt;Read the full article here →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Architecture is not about knowing the right tools. It is about asking the right questions early enough that the answers still matter.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Laravel Queue vs defer(): When to Use Each (Laravel 11, 12 &amp; 13)</title>
      <dc:creator>K. Polash</dc:creator>
      <pubDate>Mon, 30 Mar 2026 06:48:29 +0000</pubDate>
      <link>https://dev.to/polash/laravel-queue-vs-defer-when-to-use-each-laravel-11-12-13-303e</link>
      <guid>https://dev.to/polash/laravel-queue-vs-defer-when-to-use-each-laravel-11-12-13-303e</guid>
      <description>&lt;p&gt;Laravel 11 quietly introduced one of the most practical helpers in years: &lt;code&gt;defer()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It lets you run code &lt;strong&gt;after&lt;/strong&gt; the HTTP response has been sent — no queue worker, no Redis, no Supervisor config. Just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;recordOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user gets the response instantly. The closure runs after. Zero extra infrastructure.&lt;/p&gt;

&lt;p&gt;But a lot of developers are either ignoring it or misusing it. So here's the honest breakdown.&lt;/p&gt;




&lt;h2&gt;
  
  
  The One-Line Rule
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defer()&lt;/code&gt;&lt;/strong&gt; → "I'm okay if this is lost on a crash"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Job&lt;/strong&gt; → "This must succeed, and retry if it fails"&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What defer() actually does
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Runs a closure in the same PHP process, after the HTTP response is sent.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Illuminate\Support\defer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;recordOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// fires AFTER response&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// returned immediately&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key behaviours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skips execution if the response is a &lt;code&gt;4xx&lt;/code&gt; or &lt;code&gt;5xx&lt;/code&gt; (use &lt;code&gt;-&amp;gt;always()&lt;/code&gt; to override)&lt;/li&gt;
&lt;li&gt;Can be named and cancelled: &lt;code&gt;defer(fn () =&amp;gt; ..., 'name')&lt;/code&gt; + &lt;code&gt;defer()-&amp;gt;forget('name')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Doesn't survive a server crash — not persisted anywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Version support:&lt;/strong&gt; Laravel 11+ only. Not available in Laravel 10 or earlier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Queue Job&lt;/th&gt;
&lt;th&gt;defer()&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Needs queue worker&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs Redis / DB driver&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persisted to storage&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retries on failure&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delayed execution&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring (Horizon)&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero setup&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs after response&lt;/td&gt;
&lt;td&gt;separate process&lt;/td&gt;
&lt;td&gt;✅ same process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Use defer() for:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Recording analytics after an order&lt;/li&gt;
&lt;li&gt;Incrementing page view / "last seen" counters&lt;/li&gt;
&lt;li&gt;Busting a cache key after a resource is created&lt;/li&gt;
&lt;li&gt;Non-critical internal Slack notifications&lt;/li&gt;
&lt;li&gt;Lightweight audit trail writes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Queue Jobs for:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Transactional emails (must not be lost)&lt;/li&gt;
&lt;li&gt;Payment webhooks and API calls (must retry)&lt;/li&gt;
&lt;li&gt;Image/file processing (CPU heavy)&lt;/li&gt;
&lt;li&gt;Delayed tasks ("send reminder in 24 hours")&lt;/li&gt;
&lt;li&gt;Anything monitored via Horizon or Telescope&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚠️ The Swoole Gotcha
&lt;/h2&gt;

&lt;p&gt;If you use Swoole or FrankenPHP, always import Laravel's &lt;code&gt;defer()&lt;/code&gt; explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG on Swoole — calls Swoole's defer(), not Laravel's&lt;/span&gt;
&lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// CORRECT&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;Illuminate\Support\defer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swoole has its own global &lt;code&gt;defer()&lt;/code&gt;. The conflict is silent and will cost you hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Deferred Functions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_order_records_metrics&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withoutDefer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// runs deferred closures immediately&lt;/span&gt;

    &lt;span class="nc"&gt;Metrics&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;shouldReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'recordOrder'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'product_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'qty'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
         &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Full post
&lt;/h2&gt;

&lt;p&gt;For the full decision guide (including the 8-question decision matrix, Laravel 13 notes, and full code examples):&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://pola5h.github.io/blog/laravel-queue-vs-defer/" rel="noopener noreferrer"&gt;pola5h.github.io/blog/laravel-queue-vs-defer/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;What do you use &lt;code&gt;defer()&lt;/code&gt; for in your projects? Drop it in the comments 👇&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>php</category>
      <category>backend</category>
    </item>
    <item>
      <title>Laravel 13 drops March 17 — here's every new feature with code examples</title>
      <dc:creator>K. Polash</dc:creator>
      <pubDate>Sun, 08 Mar 2026 15:11:16 +0000</pubDate>
      <link>https://dev.to/polash/laravel-13-drops-march-17-heres-every-new-feature-with-code-examples-3joa</link>
      <guid>https://dev.to/polash/laravel-13-drops-march-17-heres-every-new-feature-with-code-examples-3joa</guid>
      <description>&lt;p&gt;Laravel 13 is 9 days away. Taylor Otwell announced it at Laracon EU 2026 and the headline is: &lt;strong&gt;zero breaking changes&lt;/strong&gt;. Smoothest upgrade in Laravel's history.&lt;/p&gt;

&lt;p&gt;Here's everything that's changing, with before/after code for the features that actually matter day-to-day.&lt;/p&gt;




&lt;h2&gt;
  
  
  PHP 8.3 is now the minimum
&lt;/h2&gt;

&lt;p&gt;First the housekeeping. Laravel 13 drops PHP 8.2 support. Check your version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on 8.2, upgrade your server before upgrading Laravel. Everything else in this list is non-breaking and optional.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. PHP Attributes — the headline feature
&lt;/h2&gt;

&lt;p&gt;This is the one everyone will be talking about. Instead of declaring model config, job settings, and command signatures as class properties, you can now use native PHP &lt;code&gt;#[Attribute]&lt;/code&gt; syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Models — before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'invoices'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$primaryKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'invoice_id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$keyType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$incrementing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'internal_notes'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// actual model logic starts way down here...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Models — after:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="na"&gt;#[Table('invoices', key: 'invoice_id', keyType: 'string', incrementing: false)]&lt;/span&gt;
&lt;span class="na"&gt;#[Fillable('amount', 'status', 'user_id')]&lt;/span&gt;
&lt;span class="na"&gt;#[Hidden('internal_notes')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// your real logic is immediately visible&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Jobs — before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessPayment&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'payments'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Jobs — after:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="na"&gt;#[WithQueue(connection: 'redis', queue: 'payments', tries: 3, timeout: 60)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessPayment&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// queue config declared, not buried&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Commands — before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendReportCommand&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'report:send {--force}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Send the weekly report email'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Commands — after:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="na"&gt;#[Command(signature: 'report:send {--force}', description: 'Send the weekly report email')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendReportCommand&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also works on listeners, notifications, mailables, broadcast events, requests — around 15 locations total. And again: &lt;strong&gt;fully optional&lt;/strong&gt;. Your existing code works exactly as before.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Cache::touch() — extend TTL without re-fetching
&lt;/h2&gt;

&lt;p&gt;This one's been a quiet performance footgun for years. To extend a cache item's expiry you had to fetch the value, then re-store it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before — 2 round trips, full payload transfer&lt;/span&gt;
&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_session:123'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_session:123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addHour&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel 13:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After — single EXPIRE command to Redis&lt;/span&gt;
&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_session:123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood: Redis gets one &lt;code&gt;EXPIRE&lt;/code&gt; command. Memcached uses native &lt;code&gt;TOUCH&lt;/code&gt;. Database driver runs a single &lt;code&gt;UPDATE&lt;/code&gt;. No value retrieval, no payload transfer.&lt;/p&gt;

&lt;p&gt;If you're extending TTLs on every request at scale (sliding sessions, rate limit windows, subscription checks) — this adds up fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Reverb database driver — real-time without Redis
&lt;/h2&gt;

&lt;p&gt;Scaling Laravel Reverb horizontally previously required Redis. Laravel 13 adds a database driver so you can use MySQL or PostgreSQL instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'reverb'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'scaling'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'database'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// new — no Redis required&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For small-to-medium projects that don't want to provision a separate Redis instance just for WebSockets, this is a big deal.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Passkey authentication
&lt;/h2&gt;

&lt;p&gt;Passkeys (WebAuthn — Face ID, fingerprint, hardware keys) are now built into Laravel's starter kits and Fortify. No third-party packages. Scaffolded automatically on new Laravel 13 apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Laravel AI SDK goes stable
&lt;/h2&gt;

&lt;p&gt;The Laravel AI SDK exits beta on March 17 alongside Laravel 13. First-class LLM integration (OpenAI, Anthropic, others) with proper queue support and Laravel-native conventions.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Teams support returns to starter kits
&lt;/h2&gt;

&lt;p&gt;Jetstream had Teams. The newer starter kits didn't. Laravel 13 brings it back with a cleaner implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Other improvements worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MySQL DELETE…JOIN&lt;/strong&gt; now correctly applies &lt;code&gt;ORDER BY&lt;/code&gt; and &lt;code&gt;LIMIT&lt;/code&gt; — previously silently ignored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP pool concurrency&lt;/strong&gt; defaults to &lt;code&gt;2&lt;/code&gt; instead of &lt;code&gt;null&lt;/code&gt; — pooled requests are now actually concurrent out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symfony 7.4 and 8.0&lt;/strong&gt; support&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How to upgrade
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"php"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"laravel/framework"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^13.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer update
php artisan config:clear
php artisan cache:clear
php artisan view:clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure PHP 8.3 is running first. Test on staging before touching production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full feature breakdown
&lt;/h2&gt;

&lt;p&gt;I wrote a more detailed version with upgrade notes and FAQ on my blog:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://pola5h.github.io/blog/laravel-13-new-features/" rel="noopener noreferrer"&gt;Laravel 13 New Features: Everything You Need to Know&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Which feature are you most excited about? PHP Attributes are the headline but I think &lt;code&gt;Cache::touch()&lt;/code&gt; is the one that will quietly save the most performance at scale. Drop your thoughts below.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>The Laravel Sanctum setup that actually works (and what trips most people up)</title>
      <dc:creator>K. Polash</dc:creator>
      <pubDate>Sun, 08 Mar 2026 06:47:31 +0000</pubDate>
      <link>https://dev.to/polash/the-laravel-sanctum-setup-that-actually-works-and-what-trips-most-people-up-m3e</link>
      <guid>https://dev.to/polash/the-laravel-sanctum-setup-that-actually-works-and-what-trips-most-people-up-m3e</guid>
      <description>&lt;p&gt;I've set up Sanctum in probably 8 or 9 Laravel projects at this point — payroll systems, SaaS tools, e-commerce APIs. Every time I onboard a junior dev or review someone's code, I see the same 3-4 mistakes repeated. So here's the setup that works, plus the exact things that will silently break it.&lt;/p&gt;




&lt;h2&gt;
  
  
  First: what Sanctum actually does
&lt;/h2&gt;

&lt;p&gt;Sanctum issues a plain-text token, stores a hashed version in your &lt;code&gt;personal_access_tokens&lt;/code&gt; table, and validates it on every request with a database lookup. That's it. No JWT, no signatures, no decoding — just a DB row.&lt;/p&gt;

&lt;p&gt;This is why token revocation is trivially easy with Sanctum and nightmarish with JWT. Delete the row, the token is dead instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup (Laravel 12)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Install (Laravel 10 and below only)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel 11 and 12 ship with Sanctum already. If you're on 10:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/sanctum
php artisan vendor:publish &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Laravel&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="s2"&gt;anctum&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="s2"&gt;anctumServiceProvider"&lt;/span&gt;
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — Add the trait to your User model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one people forget most often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Sanctum\HasApiTokens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasApiTokens&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, &lt;code&gt;createToken()&lt;/code&gt; doesn't exist on your model, and you'll get a cryptic error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Routes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Public&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth/register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'register'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// Protected&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth/logout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'logout'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth/me'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'me'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 — The AuthController&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'email'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Invalid credentials'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth_token'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plainTextToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'user'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;currentAccessToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Logged out'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5 — Send the token&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorization: Bearer 1|yourtokenhere...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the format — &lt;code&gt;1|randomstring&lt;/code&gt;. The number is the token ID in the database. Send the full string.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 4 things that will silently break your setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Forgetting &lt;code&gt;HasApiTokens&lt;/code&gt; on the User model&lt;/strong&gt;&lt;br&gt;
You'll get a "method not found" error on &lt;code&gt;createToken()&lt;/code&gt;. Always check this first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Sending the token wrong&lt;/strong&gt;&lt;br&gt;
It must be &lt;code&gt;Authorization: Bearer YOUR_TOKEN&lt;/code&gt;. Not a query param, not a cookie, not &lt;code&gt;Token YOUR_TOKEN&lt;/code&gt;. Bearer, exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Not running migrations&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;personal_access_tokens&lt;/code&gt; table doesn't exist yet. Run &lt;code&gt;php artisan migrate&lt;/code&gt; and confirm the table is there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. CORS blocking your frontend&lt;/strong&gt;&lt;br&gt;
Add your frontend URL to &lt;code&gt;config/cors.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'allowed_origins'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'https://yourfrontend.com'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'supports_credentials'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Revoking tokens
&lt;/h2&gt;

&lt;p&gt;This is where Sanctum shines vs JWT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Logout current device&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;currentAccessToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Logout all devices&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With JWT, you'd need a denylist and extra infrastructure. With Sanctum, it's one line.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting token expiry
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;config/sanctum.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Expire after 60 days (value is in minutes)&lt;/span&gt;
&lt;span class="s1"&gt;'expiration'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// Never expire&lt;/span&gt;
&lt;span class="s1"&gt;'expiration'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Should you use Sanctum or JWT?
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;Sanctum&lt;/strong&gt; if you're building an API for your own app (SPA, mobile). Use &lt;strong&gt;JWT&lt;/strong&gt; only if you need stateless cross-service auth in a microservices setup. For 90% of Laravel projects, Sanctum is the right call.&lt;/p&gt;

&lt;p&gt;I wrote a full breakdown here: &lt;a href="https://pola5h.github.io/blog/laravel-api-authentication-jwt-sanctum/" rel="noopener noreferrer"&gt;Does Laravel Sanctum use JWT? →&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Full tutorial
&lt;/h2&gt;

&lt;p&gt;If you want the complete version with register, me endpoint, token abilities, and more — I wrote it up here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://pola5h.github.io/blog/laravel-sanctum-tutorial/" rel="noopener noreferrer"&gt;Laravel Sanctum Setup Tutorial (2026)&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Already using Sanctum?
&lt;/h2&gt;

&lt;p&gt;I built a &lt;a href="https://pola5h.github.io/laravel-api-starter-kit/" rel="noopener noreferrer"&gt;Laravel API Starter Kit&lt;/a&gt; with Sanctum pre-configured — auth, roles, pagination, API versioning all included. Saves a few hours on every new project. $19 on Gumroad if you want to check it out.&lt;/p&gt;




&lt;p&gt;What issues have you run into with Sanctum? Drop them below — happy to help debug.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>blade-flags gives you SVG rectangles. This gives you flags users actually recognize.</title>
      <dc:creator>K. Polash</dc:creator>
      <pubDate>Fri, 06 Mar 2026 03:57:03 +0000</pubDate>
      <link>https://dev.to/polash/blade-flags-gives-you-svg-rectangles-this-gives-you-flags-users-actually-recognize-3ca1</link>
      <guid>https://dev.to/polash/blade-flags-gives-you-svg-rectangles-this-gives-you-flags-users-actually-recognize-3ca1</guid>
      <description>&lt;p&gt;There's already a popular Laravel flag package. &lt;code&gt;outhebox/blade-flags&lt;/code&gt; has 663k installs, and it works fine.&lt;/p&gt;

&lt;p&gt;So why did I build another one?&lt;/p&gt;

&lt;p&gt;Because SVG rectangular flags and emoji flags solve &lt;strong&gt;different problems&lt;/strong&gt; — and most Laravel apps actually need the emoji version.&lt;/p&gt;




&lt;h2&gt;
  
  
  The difference nobody talks about
&lt;/h2&gt;

&lt;p&gt;When your user sees this in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-flag-icon type="country" code="bd" width="24px" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They see: 🇧🇩&lt;/p&gt;

&lt;p&gt;That's the flag they see in WhatsApp. In Twitter. In their phone keyboard. In every modern app, they use it daily. Their brain recognizes it in zero milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;blade-flags&lt;/code&gt; renders a tiny rectangular SVG of the Bangladeshi flag — accurate, technically correct, and completely unfamiliar to most users. It looks like something from a government website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emoji flags match the visual language of the modern web. SVG flags don't.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  When does this actually matter?
&lt;/h2&gt;

&lt;p&gt;If you're building a &lt;strong&gt;developer tool or admin dashboard&lt;/strong&gt; — use &lt;code&gt;blade-flags&lt;/code&gt;. SVG flags look sharp and professional at small sizes in dense UIs.&lt;/p&gt;

&lt;p&gt;If you're building anything &lt;strong&gt;user-facing&lt;/strong&gt; — locale switchers, user profiles, country selectors, chat apps, language pickers — use Twemoji flags. Users recognize them instantly. That recognition reduces friction.&lt;/p&gt;

&lt;p&gt;The mental model test: your user doesn't think "ISO 3166-1 alpha-2 code for Bangladesh." They think 🇧🇩. Build for that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require pola5h/laravel-flags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auto-registered on Laravel 5.5+. No config publishing, no setup. Done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- Country flag --}}
&amp;lt;x-flag-icon type="country" code="us" width="24px" height="24px" /&amp;gt;

{{-- Language flag --}}
&amp;lt;x-flag-icon type="language" code="bn" width="24px" height="24px" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two parameters that matter: &lt;code&gt;type&lt;/code&gt; (country or language) and &lt;code&gt;code&lt;/code&gt; (ISO 2-letter code). Width and height are optional — they default to auto.&lt;/p&gt;




&lt;h2&gt;
  
  
  Country flags
&lt;/h2&gt;

&lt;p&gt;Any ISO 3166-1 alpha-2 code works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-flag-icon type="country" code="us" width="20px" /&amp;gt; 🇺🇸 United States
&amp;lt;x-flag-icon type="country" code="gb" width="20px" /&amp;gt; 🇬🇧 United Kingdom
&amp;lt;x-flag-icon type="country" code="bd" width="20px" /&amp;gt; 🇧🇩 Bangladesh
&amp;lt;x-flag-icon type="country" code="de" width="20px" /&amp;gt; 🇩🇪 Germany
&amp;lt;x-flag-icon type="country" code="jp" width="20px" /&amp;gt; 🇯🇵 Japan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dynamic usage from a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-flag-icon type="country" code="{{ $user-&amp;gt;country_code }}" width="20px" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;250+ countries supported.&lt;/p&gt;




&lt;h2&gt;
  
  
  Language flags
&lt;/h2&gt;

&lt;p&gt;Language flags map to the country most associated with that language. Bengali → Bangladesh, Arabic → Saudi Arabia, French → France.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-flag-icon type="language" code="en" width="20px" /&amp;gt; English
&amp;lt;x-flag-icon type="language" code="bn" width="20px" /&amp;gt; Bengali
&amp;lt;x-flag-icon type="language" code="ar" width="20px" /&amp;gt; Arabic
&amp;lt;x-flag-icon type="language" code="zh" width="20px" /&amp;gt; Chinese
&amp;lt;x-flag-icon type="language" code="fr" width="20px" /&amp;gt; French
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dynamic with the current app locale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;x-flag-icon type="language" code="{{ app()-&amp;gt;getLocale() }}" width="20px" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;100+ languages supported.&lt;/p&gt;




&lt;h2&gt;
  
  
  The use case I built this for: locale switchers
&lt;/h2&gt;

&lt;p&gt;This is where the difference is most visible. A locale switcher with SVG flags feels like a dropdown from 2012. With Twemoji flags it feels native — like switching language on your iPhone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@foreach ($availableLocales as $locale)
    &amp;lt;a href="{{ route('locale.switch', $locale) }}"
       class="{{ app()-&amp;gt;getLocale() === $locale ? 'active' : '' }}"&amp;gt;
        &amp;lt;x-flag-icon
            type="language"
            code="{{ $locale }}"
            width="18px"
            height="18px"
        /&amp;gt;
        {{ strtoupper($locale) }}
    &amp;lt;/a&amp;gt;
@endforeach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, familiar, instant recognition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Side-by-side comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;pola5h/laravel-flags&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;outhebox/blade-flags&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flag style&lt;/td&gt;
&lt;td&gt;Twemoji emoji 🇧🇩&lt;/td&gt;
&lt;td&gt;SVG rectangular&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;User-facing UI&lt;/td&gt;
&lt;td&gt;Admin/dev tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Countries&lt;/td&gt;
&lt;td&gt;250+&lt;/td&gt;
&lt;td&gt;300+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language flags&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero config&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Installs&lt;/td&gt;
&lt;td&gt;New&lt;/td&gt;
&lt;td&gt;663k&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one thing &lt;code&gt;blade-flags&lt;/code&gt; doesn't have at all: &lt;strong&gt;language flags&lt;/strong&gt;. If you need to show a flag for Bengali, Arabic, or French — not the country, but the language — this package is currently your only option in the Laravel ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Pola5h/FlagIcons" rel="noopener noreferrer"&gt;github.com/Pola5h/FlagIcons&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Docs &amp;amp; demo&lt;/strong&gt;: &lt;a href="https://pola5h.github.io/FlagIcons/" rel="noopener noreferrer"&gt;pola5h.github.io/FlagIcons&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📥 &lt;strong&gt;Install&lt;/strong&gt;: &lt;code&gt;composer require pola5h/laravel-flags&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Both packages solve real problems. Use SVG flags when precision matters for developers. Use Twemoji flags when recognition matters for users.&lt;/p&gt;

&lt;p&gt;If you're building something user-facing, give it a try — it's free, MIT licensed, and takes 30 seconds to add to any Laravel project.&lt;/p&gt;

&lt;p&gt;What are you building it into? I'd love to see it in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>php</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Structure Every Laravel REST API Project</title>
      <dc:creator>K. Polash</dc:creator>
      <pubDate>Thu, 05 Mar 2026 15:14:53 +0000</pubDate>
      <link>https://dev.to/polash/how-i-structure-every-laravel-rest-api-project-33n1</link>
      <guid>https://dev.to/polash/how-i-structure-every-laravel-rest-api-project-33n1</guid>
      <description>&lt;p&gt;Every time I start a new Laravel API project, I used to spend half a day on the same setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sanctum authentication&lt;/li&gt;
&lt;li&gt;Spatie roles &amp;amp; permissions&lt;/li&gt;
&lt;li&gt;Consistent JSON error responses&lt;/li&gt;
&lt;li&gt;API versioning&lt;/li&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After doing this on project after project, I finally settled on a structure that works every time. Here's exactly how I do it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Consistent JSON Response Format
&lt;/h2&gt;

&lt;p&gt;The first thing I set up is a trait that forces every endpoint to return the same JSON structure. Nothing is more annoying than an API that returns different formats for success vs errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Traits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;trait&lt;/span&gt; &lt;span class="nc"&gt;ApiResponseTrait&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;successResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'data'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'meta'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'version'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'timestamp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$statusCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Error'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'errors'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'meta'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'version'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'timestamp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$statusCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;notFoundResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Resource not found'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;unauthorizedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Unauthorized'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;validationErrorResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Validation failed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;errorResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every controller uses this trait. No more inconsistent responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Global Exception Handler
&lt;/h2&gt;

&lt;p&gt;Instead of handling exceptions in every controller, I catch everything in one place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Exceptions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Auth\AuthenticationException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\ModelNotFoundException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Foundation\Exceptions\Handler&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;ExceptionHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Validation\ValidationException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpKernel\Exception\NotFoundHttpException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ExceptionHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expectsJson&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;ModelNotFoundException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                    &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Resource not found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;AuthenticationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                    &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Unauthenticated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;ValidationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                    &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Validation failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'errors'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;NotFoundHttpException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                    &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Route not found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every exception returns clean JSON automatically — no more ugly HTML error pages in your API.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. API Versioning Structure
&lt;/h2&gt;

&lt;p&gt;I always separate routes and controllers by version. This way future changes don't break existing clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/Http/Controllers/Api/
├── V1/
│   ├── AuthController.php
│   └── UserController.php
└── V2/  ← ready for the future
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;routes/api.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Api\V1\AuthController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Api\V1\UserController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Public auth routes&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'throttle:auth'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'register'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Protected routes&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth/logout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'logout'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth/me'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'me'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;UserController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, versioned, easy to extend.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Sanctum Authentication
&lt;/h2&gt;

&lt;p&gt;I use Laravel Sanctum for all API authentication. Here's a clean AuthController:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers\Api\V1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Controllers\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Requests\Api\LoginRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Requests\Api\RegisterRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Traits\ApiResponseTrait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;ApiResponseTrait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RegisterRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'email'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Hash&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assignRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api-token'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plainTextToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;successResponse&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'user'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'Registration successful'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;LoginRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unauthorizedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid credentials'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$user&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api-token'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;plainTextToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;successResponse&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'user'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'Login successful'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;currentAccessToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;successResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Logged out successfully'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;successResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'roles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'permissions'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Spatie Roles &amp;amp; Permissions
&lt;/h2&gt;

&lt;p&gt;I always use &lt;code&gt;spatie/laravel-permission&lt;/code&gt; for roles. Here's my seeder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Database\Seeders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Seeder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Spatie\Permission\Models\Permission&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Spatie\Permission\Models\Role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RolePermissionSeeder&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Seeder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Create permissions&lt;/span&gt;
        &lt;span class="nv"&gt;$permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'view users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'edit users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'delete users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'manage roles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$permissions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$permission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Permission&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;firstOrCreate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$permission&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Create roles and assign permissions&lt;/span&gt;
        &lt;span class="nv"&gt;$admin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;firstOrCreate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nv"&gt;$admin&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;syncPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$permissions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$editor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;firstOrCreate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nv"&gt;$editor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;syncPermissions&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'view users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'edit users'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;firstOrCreate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;php artisan db:seed --class=RolePermissionSeeder&lt;/code&gt; and you have a full role system ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Rate Limiting
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;app/Providers/RouteServiceProvider.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stricter limits on auth routes to prevent brute force. General API routes get 60 requests per minute.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;Every response from my API looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"view users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit users"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-05T10:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, consistent, predictable. Every endpoint. Every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This structure took me several projects to settle on. The key principles are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One response format&lt;/strong&gt; — use a trait, enforce it everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One place for exceptions&lt;/strong&gt; — global handler, not per-controller&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version from day one&lt;/strong&gt; — you'll thank yourself later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles from day one&lt;/strong&gt; — adding them later is painful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't want to set all this up manually, I packaged everything into a ready-to-use starter kit — migrations, seeders, controllers, Postman collection, and README all included.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://pola5h.github.io/laravel-api-starter-kit/" rel="noopener noreferrer"&gt;Laravel API Starter Kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What does your Laravel API structure look like? I'd love to hear how others approach this in the comments.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>laravel</category>
      <category>webdev</category>
      <category>php</category>
      <category>api</category>
    </item>
  </channel>
</rss>
