Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Api Boilerplate Jwt Laravel Package

francescomalatesta/laravel-api-boilerplate-jwt

A Laravel API boilerplate that ships with JWT authentication out of the box. It provides a ready-to-use RESTful foundation with login, registration, token refresh, CORS, and a full PHPUnit test suite, so you can skip the setup and focus on building your API.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a project skeleton, not a composer require package. You consume it once, at project birth, and then you own every file.

composer create-project --prefer-dist francescomalatesta/laravel-api-boilerplate-jwt my-api
cd my-api
cp .env.example .env
php artisan key:generate
php artisan jwt:secret          # writes JWT_SECRET — do not skip
# set DB_* in .env, then:
php artisan migrate
php artisan serve

First places to open, in this order:

  1. routes/api.php — Dingo API versioned route groups ($api->version('v1', ...)). This is where every endpoint you add lives.
  2. app/Api/V1/Controllers/Controller.php — the base controller with the Fractal/Dingo response helpers ($this->response->item(...), ->collection(...)). Every controller you write extends this.
  3. app/Api/V1/Controllers/AuthController.php — signup, login, refresh, me, logout. It is the reference implementation for everything else.
  4. app/Api/V1/Requests/ and app/Api/V1/Transformers/ — validation and output shaping, mirroring the controller structure.
  5. config/api.php (Dingo: prefix, version, throttling) and config/jwt.php (TTL, blacklist, algo).

First use case — prove the whole stack in one round trip:

curl -X POST http://localhost:8000/api/v1/auth/login \
  -H "Accept: application/json" \
  -d "[email protected]&password=password"

curl http://localhost:8000/api/v1/auth/me \
  -H "Accept: application/json" \
  -H "Authorization: Bearer <access_token>"

If me returns your user, JWT + middleware + transformer are all wired correctly and you can start modelling real resources.

Implementation Patterns

The scaffold-and-edit loop. The author's companion generator (francescomalatesta/laravel-api-generator) is the point of the boilerplate: php artisan api:generate Post --with=... emits migration, model, request, transformer, controller, and route entry in one shot. Treat the output as a first draft — you will rewrite the transformer and add authorization on nearly every resource. The generator is a dev-time tool with no runtime footprint, so generated code is yours to refactor freely.

Adding a resource end to end:

  1. Migration + model (or api:generate).
  2. FormRequest in app/Api/V1/Requests/ — Laravel rejects invalid input before your controller runs; Dingo's exception handler formats the 422.
  3. Transformer in app/Api/V1/Transformers/ — never return a model or ->toArray(). Whitelist fields here; this is your only defence against leaking password, remember_token, or internal IDs.
  4. Controller method extending the base controller, returning $this->response->item($post, new PostTransformer) / ->collection(...).
  5. Route inside the existing v1 group, with jwt.auth if it's private.

Routing and versioning. Everything lives inside $api->version('v1', function ($api) { ... }). To ship a v2, add a second group pointing at App\Api\V2\Controllers and leave v1 running — that's the whole migration story, and it's a genuinely good one. Group middleware per concern:

$api->group(['middleware' => 'jwt.auth'], function ($api) {
    $api->get('posts', 'PostController@index');
});

Auth flow. Login/signup return access_token + token_type: bearer + expires_in. Protected routes use jwt.auth; the refresh endpoint uses jwt.refresh and hands back a rotated token. Logout invalidates via JWTAuth::invalidate(JWTAuth::getToken()), which only does anything if blacklist_enabled is true in config/jwt.php.

Scoping to the current user. Because jwt.auth populates the guard, auth()->user() works normally inside controllers and policies — use it in queries (Post::where('user_id', auth()->id())) rather than trusting an ID from the payload.

Throttling. Wrap login/signup in Dingo's api.throttle middleware. Without it, the auth endpoints are an open brute-force target out of the box.

Gotchas and Tips

  • The version ceiling is the real cost. This is Laravel 5.x-era code (Dingo API + tymon/jwt-auth 1.x, PHP 7.1–7.3 vintage). It will not drop into a Laravel 10/11 app, and composer update will not carry you forward. Budget for a fork-and-modernize pass, or treat it as a reference architecture you read rather than a foundation you build on. "0 dependents" is meaningless here — create-project templates never register as dependents — so judge it on the last commit date, not that number.
  • jwt:secret is mandatory. A missing JWT_SECRET surfaces as confusing "token could not be parsed" errors rather than a clear config failure. Same for API_PREFIX/API_VERSION if you change the Dingo prefix: the routes move and your client's hardcoded /api/v1 silently 404s.
  • Dingo hides routes from route:list. Use php artisan api:routes to see what's actually registered. If an endpoint 404s, check this before touching the controller — the usual cause is a route added outside the $api->version closure, or plain Laravel route syntax mixed into the Dingo file.
  • Handle TokenExpiredException explicitly. By default an expired token can bubble up as a 500 or an opaque 401. Catch it in app/Exceptions/Handler.php and return a distinguishable payload ({"error": "token_expired"}) so clients know to refresh instead of logging the user out.
  • Refresh races break clients. With blacklisting on, /auth/refresh invalidates the old token immediately — two parallel requests from the same stale token means one gets a 401 and the user is bounced. Fix on both sides: blacklist_grace_period in config/jwt.php server-side, single-flight refresh in the client.
  • Blacklist growth. The default file cache driver means expired tokens accumulate in storage/framework/cache. Switch to redis or the database cache driver before production.
  • Adding a user field touches four files: migration, User $fillable, SignUpRequest rules, and UserTransformer. Forgetting the transformer is the classic "I saved it but the API doesn't show it" bug — the field is in the DB the whole time.
  • CORS preflight. If a browser SPA gets 401s on OPTIONS, the CORS middleware isn't attached to the api group; it's a middleware registration issue, not auth.
  • Token storage. This boilerplate is bearer-token oriented, so no CSRF protection — which means tokens in localStorage are XSS-extractable. For first-party SPAs, consider moving to httpOnly cookies; for third-party/mobile clients, bearer is correct.
  • No extension points, only files. There is no runtime package to hook into — your customization surface is the base Controller, the transformers, and Handler. Keep those three clean and the rest of the codebase stays replaceable.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource