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.
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:
routes/api.php — Dingo API versioned route groups ($api->version('v1', ...)). This is where every endpoint you add lives.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.app/Api/V1/Controllers/AuthController.php — signup, login, refresh, me, logout. It is the reference implementation for everything else.app/Api/V1/Requests/ and app/Api/V1/Transformers/ — validation and output shaping, mirroring the controller structure.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.
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:
api:generate).app/Api/V1/Requests/ — Laravel rejects invalid input before your controller runs; Dingo's exception handler formats the 422.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.$this->response->item($post, new PostTransformer) / ->collection(...).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.
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.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.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./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.storage/framework/cache. Switch to redis or the database cache driver before production.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.OPTIONS, the CORS middleware isn't attached to the api group; it's a middleware registration issue, not auth.localStorage are XSS-extractable. For first-party SPAs, consider moving to httpOnly cookies; for third-party/mobile clients, bearer is correct.Controller, the transformers, and Handler. Keep those three clean and the rest of the codebase stays replaceable.How can I help you explore Laravel packages today?