All articles

Building a REST API with Laravel: A Practical Guide

How to structure a Laravel API that mobile and frontend teams enjoy using: Sanctum authentication, Form Request validation, API Resources, consistent errors and status codes, pagination, rate limiting, authorization policies, documentation, and feature tests.

Web Development|Published |10 min read
HTML and Blade code on a dark editor screen

Laravel makes it easy to return JSON from a controller, and that is where many API projects stop thinking about design. A year later the mobile team handles four different error formats, the web app breaks whenever a column is renamed, and nobody is sure which endpoints check permissions. A few conventions set up at the start avoid most of this. The examples below assume Laravel 11 or newer, but the ideas apply to older versions too.

Set Up the API and Version It

Since Laravel 11 a new application does not include API routes by default. Running php artisan install:api creates routes/api.php and installs Laravel Sanctum. Put the routes under a version prefix such as /api/v1 from the first day. Mobile apps stay installed for months without updating, so when a response has to change in a breaking way you can add v2 while old app versions keep working on v1.

Choose the Right Sanctum Mode

ClientAuthenticationNotes
Mobile appSanctum personal access tokensIssue a token at login, store it in the device keychain or keystore, and give it abilities and an expiry
Web frontend on the same top-level domainSanctum SPA authentication with cookiesUses the session cookie and CSRF protection, so no token is stored in JavaScript
Another server or partner systemTokens with limited abilities, or Laravel Passport if partners need OAuthOne token per partner so each one can be revoked on its own

Avoid storing tokens in browser localStorage. Any script injected into the page can read it. Cookie-based SPA authentication is the safer default for a web frontend.

Validate With Form Requests

Move validation out of the controller into Form Request classes created with php artisan make:request. The rules sit next to the authorize method, controllers stay short, and the same request class documents what the endpoint accepts. When the client sends the Accept: application/json header, failed validation returns a 422 response with an errors object keyed by field name, which frontend and mobile developers can map straight onto form fields. Ask every client to send that header, otherwise Laravel may answer a failed request with a redirect.

Shape Responses With API Resources

Returning Eloquent models directly exposes every column, including ones you add later, and ties the API to the database structure. API Resources (php artisan make:resource) define exactly which fields go out and under which names. You can rename a column without changing the API, hide internal fields, format dates in one place, and include relations only when they were loaded with whenLoaded. Collections of resources also add pagination links and metadata automatically.

Use Status Codes Consistently

SituationStatus code
Read or update succeeded200 OK
A new record was created201 Created
Delete succeeded with no body204 No Content
Not logged in or the token is invalid401 Unauthorized
Logged in but not allowed to do this403 Forbidden
The record does not exist404 Not Found
Validation failed422 Unprocessable Content
Too many requests429 Too Many Requests

Keep one error shape for everything that is not a validation error, for example a message field and an optional code field that the client can check. In Laravel 11 and later you can customise how exceptions render for API routes in bootstrap/app.php. Make sure stack traces never reach clients by keeping APP_DEBUG off in production.

Authorize Every Endpoint

The most common serious bug in APIs we review is an endpoint that checks the user is logged in but not whether the record belongs to them. Changing the ID in the URL returns another customer's order. Write a Policy for each model and call it in every controller method or Form Request, and add a feature test that requests another user's record and expects a 403 or 404.

Pagination, Filtering, and Performance

  • Never return an unbounded list. Use paginate for pages with totals, or cursorPaginate for long feeds and infinite scroll, which stays fast on large tables.
  • Set a maximum page size on the server so a client cannot ask for 100,000 rows.
  • Load relations with with() and enable Model::preventLazyLoading() outside production, so N+1 queries fail during development.
  • Add database indexes for the columns used in filters and sorting.
  • Cache responses that are expensive and change rarely, such as reference data and settings.

Rate Limiting

Define limiters with RateLimiter::for in a service provider and attach them with the throttle middleware. Use a strict limit per IP address on login, OTP, and password reset endpoints, and a general limit per user for the rest of the API. Clients receive a 429 response with a Retry-After header when they exceed it, so the mobile app can wait and retry instead of hammering the server.

Document and Test It

  1. 1Generate an OpenAPI document from the code with a package such as Scramble, so the documentation follows the Form Requests and Resources instead of drifting from them.
  2. 2Share the OpenAPI file with the frontend and mobile teams so they can generate typed clients.
  3. 3Write feature tests with getJson and postJson for every endpoint, covering success, validation errors, and access by the wrong user.
  4. 4Run the tests in CI on every pull request, together with a check that the OpenAPI document still generates.

Key takeaways

  • Version the API from the start and use Sanctum tokens for mobile and cookies for web.
  • Validate with Form Requests and shape output with API Resources instead of returning models.
  • Use consistent status codes and one error format, with debug output off in production.
  • Check ownership with Policies on every endpoint and test access by the wrong user.
  • Paginate everything, prevent N+1 queries, rate limit sensitive endpoints, and generate OpenAPI docs from the code.

Related articles

More articles on software development, AI, cloud, and infrastructure.

A smartphone home screen full of app icons
Mobile Development|

How to Publish an App to Google Play and the App Store

What you need before submitting a mobile app: developer accounts for a person or a company, signing keys, store listings and privacy forms, the closed testing rule for new Play accounts, TestFlight, common App Store rejections, and staged releases.

A laptop showing a business dashboard next to a coffee mug
Software Development|

Custom Software vs SaaS: How to Decide for Your Business

When an off-the-shelf SaaS product is the better buy, when custom software pays for itself, how to compare costs over several years, the hybrid route of SaaS plus custom integrations, and the questions to ask before signing either.

Looking for a software development partner?

Tell us about your project, what you need to build, and the challenges you are facing. We can discuss the technical approach, scope, timeline, and estimated cost.

Start a conversation