citizen63000/easy-api-core
Symfony bundle with core utilities for REST APIs: entity config introspection from Doctrine or database schema, form serialization for dynamic frontends, an abstract command base, and helpers for persistence, case conversion, and pluralization.
The README is a single heading, so treat the package source as the documentation. Do this before writing a line of application code:
composer require citizen63000/easy-api-core
Then immediately answer three questions from the installed code, not from blog posts or the vendor's docs:
# 1. What does it actually require?
cat vendor/citizen63000/easy-api-core/composer.json
# 2. What does it actually expose?
find vendor/citizen63000/easy-api-core/src -name "*.php" | head -50
ls vendor/citizen63000/easy-api-core/src/Facades
# 3. Is it registered, and what did it register?
php artisan about
php artisan list | grep -i easy
The release note 2.0 — sf6.4 implies a Symfony 6.4 floor, which maps to Laravel 11.x. Confirm the illuminate/* constraints in composer.json against your composer.lock before you commit to anything; a package pinned to a narrow Laravel window is the single most likely reason this integration fails on day two.
Do not adopt this package across an existing API. Prove it on a single new endpoint in a throwaway branch, and build the same endpoint twice — once vanilla, once with the package — so you can measure the actual delta rather than the advertised one.
Vanilla baseline:
// app/Http/Resources/ExampleResource.php
class ExampleResource extends JsonResource
{
public function toArray($request): array
{
return ['id' => $this->id, 'name' => $this->name];
}
}
// routes/api.php
Route::get('/examples', fn () => ExampleResource::collection(Example::paginate()));
Package version (verify class names and facade methods against vendor/ first — nothing below is documented upstream):
use Citizen63000\EasyApiCore\Http\Resources\BaseResource;
class ExampleResource extends BaseResource
{
public function toArray($request): array
{
return ['id' => $this->id, 'name' => $this->name];
}
}
If BaseResource exists in src/Http/Resources/, check whether it extends Illuminate\Http\Resources\Json\JsonResource. That single fact determines nearly everything in the sections below: if it does, you are buying convention, not capability, and you can always fall back to vanilla. If it doesn't, you are taking on a parallel serialization model and the migration-out cost goes up sharply.
The most defensible way to use a zero-star package in production is to make it a dependency of your abstraction, not of your controllers. Introduce two app-owned base classes and extend the package only through them:
// app/Api/Resources/ApiResource.php
namespace App\Api\Resources;
use Citizen63000\EasyApiCore\Http\Resources\BaseResource as PackageResource;
abstract class ApiResource extends PackageResource
{
// your envelope, your meta keys, your timestamp format
}
Every resource in the app extends App\Api\Resources\ApiResource. When the package stalls or forces a breaking upgrade, you change one file instead of two hundred. This is the whole strategy in one sentence.
Use the package for the response layer only. Business logic, queries, and authorization stay in your actions/services/controllers where you can test them without the package in the stack:
public function index(IndexExamplesRequest $request)
{
$examples = Example::query()
->when($request->filled('search'), fn ($q) => $q->where('name', 'like', "%{$request->search}%"))
->paginate($request->integer('per_page', 15));
return ExampleResource::collection($examples);
}
If the package supplies a pagination helper (e.g. EasyApi::paginate(...)), benchmark it against Resource::collection($paginator). Laravel already merges pagination metadata automatically; a wrapper that re-shapes it will silently break clients expecting data / meta / links. Only reach for the helper if it does something Resource::collection() genuinely cannot.
Request validation is the one area where these packages usually add real value (consistent 422 shapes, Accept: application/json handling). But Illuminate\Foundation\Http\FormRequest is already well-tested. If BaseRequest adds nothing beyond a failedValidation override, copy that override into an app-owned base request and skip the inheritance:
class ApiRequest extends FormRequest
{
protected function failedValidation(Validator $validator): void
{
throw new HttpResponseException(response()->json([
'message' => 'The given data was invalid.',
'errors' => $validator->errors(),
], 422));
}
}
Two benefits: no dependency for behaviour you can read in ten lines, and no risk that a future release changes your error contract under you.
Don't let a facade manage versions. Route prefixes are free and greppable:
Route::prefix('v1')->group(base_path('routes/api_v1.php'));
Route::prefix('v2')->group(base_path('routes/api_v2.php'));
If the package ships a setVersion()-style helper, it is almost certainly storing state on a singleton — which means the version depends on what ran earlier in the request lifecycle. That is a debugging trap, not a feature.
If ApiTransformResponse or ApiAuthenticate (or similarly named) middleware exists, add it to a single throwaway route group first and read its handle() source. Response-transforming middleware is the highest-risk component in any API package: it touches every response, including errors, redirects, and streaming. Never attach it to a group that also serves Inertia, file downloads, or health checks.
Whatever mocking helpers ship with the package, your test suite should assert on the HTTP boundary:
it('returns a paginated list of examples', function () {
Example::factory()->count(3)->create();
$this->getJson('/api/v1/examples')
->assertOk()
->assertJsonStructure([
'data' => [['id', 'name']],
]);
});
These tests survive the package being removed. Tests written against EasyApi::mockResource() do not.
The README is # easy-api-core and nothing else. Every method name circulating in third-party write-ups — EasyApi::resource(), EasyApi::paginate(), EasyApi::fields(), EasyApi::dumpResource(), EasyApi::cacheResource(), BaseRequest, ApiTransformResponse — is unverified. Before you write code against any of them:
grep -rn "function resource\|function paginate\|function fields" vendor/citizen63000/easy-api-core/src
If a method isn't there, the facade will throw BadMethodCallException at runtime, not at boot, so a green test suite can still hide a broken endpoint. Add one smoke test per facade method you use.
sf6.4 in the release note means a Symfony 6.4 baseline, so expect illuminate/* ^11.0. Consequences:
composer why-not citizen63000/easy-api-core in the target app before committing.Pin it exactly ("citizen63000/easy-api-core": "2.0.*"), commit the lock file, and treat minor bumps as reviewable changes.
No stars, no dependents, no visible test suite. Assume:
vendor/ source rather than searching for answers.Keep an UPGRADING.md note in your repo listing every package class you actually reference. It should be short. If it grows past a dozen entries, that's the signal to reassess.
JsonResource::collection($paginator) already emits links and meta. If a package pagination helper wraps that again, you get data.data or duplicated meta keys, and frontend consumers break in ways that are tedious to trace. Assert the exact JSON shape in a test on day one:
$this->getJson('/api/v1/examples')
->assertJsonPath('meta.current_page', 1)
->assertJsonMissingPath('data.data');
If EasyApi::cacheResource() exists, be careful. Serialized resources capture their underlying model at construction time and serialize closures/whenLoaded state inconsistently. Cache the query result or the rendered array, not the resource object:
$payload = Cache::remember("examples.{$id}", 300, fn () => (new ExampleResource($example))->resolve());
Symptom of getting this wrong: stale relationships, missing whenLoaded data, or a fatal on unserialize after a deploy.
php artisan tinker
>>> (new App\Http\Resources\ExampleResource(App\Models\Example::first()))->resolve();
resolve() is stock Laravel and always tells you the truth about what your resource emits. If the package advertises a dumpResource() helper, verify it exists first; otherwise you'll waste an hour on a method that was invented by a language model summarizing the repo.
For request-level debugging, php artisan route:list --path=api plus a Log::info() at the top of any package middleware will out-perform guessing. If the package has a config flag like log_enabled, publish the config and read the keys:
php artisan vendor:publish --tag=config --provider="Citizen63000\EasyApiCore\EasyApiCoreServiceProvider"
If the provider class doesn't exist under that namespace, the publish command fails loudly — which is itself a useful five-second test of how much of the documented surface is real.
Two customisations are low-risk because they sit at boundaries you already control:
{ data, meta, errors } defined once in App\Api\Resources\ApiResource, so the package can be swapped without touching endpoints.bootstrap/app.php (Laravel 11 style) or your exception handler, mapping ValidationException, AuthenticationException, and ModelNotFoundException to consistent JSON. Doing this yourself costs ~30 lines and removes your dependence on any package-provided handler.For a greenfield Laravel project with a predictable CRUD surface, this package saves you maybe half a day over vanilla JsonResource + FormRequest + a small exception renderer — and costs you a dependency with no community, a narrow framework window, and an unverifiable API surface. Try it on one non-critical endpoint. If BaseResource extends JsonResource and the facade methods actually exist, it's a reasonable convention layer; keep the wrap in place and you can leave whenever you want. If the source is thinner than the write-ups suggest, you've lost an afternoon and learned something useful about how these package assessments get written.
How can I help you explore Laravel packages today?