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 Toolkit Laravel Package

juststeveking/laravel-api-toolkit

A collection of tools and helpers for building APIs with Laravel. Streamline common tasks like responses, resources, and formatting so your endpoints stay consistent, clean, well-structured, and easy to maintain as your application grows. A handy companion for teams standardising their API layer.

View on GitHub
Deep Wiki
Context7

Getting Started

First, verify what actually shipped. The description paints an API toolkit (response envelopes, resources, query helpers, error handling), but the v1.0.0 release note says the release "contains only the one helpful artisan command." Those are different products. Do not plan against the README; plan against vendor/.

  1. Install pinned, into one repo, behind a spike branch.

    composer require juststeveking/laravel-api-toolkit:1.0.0
    

    Exact version, no caret. There is a single release and therefore no upgrade contract to infer command-signature stability from.

  2. Read the constraints before you commit to the spike. A December 2020 release implies Laravel 8-era support. Open vendor/juststeveking/laravel-api-toolkit/composer.json and check the illuminate/* and php constraints against your current matrix. This is the single most likely reason the spike dies, and it takes thirty seconds.

  3. Confirm registration. Package discovery is the expected path, so there is probably nothing to add to config/app.php. Prove it:

    php artisan list | grep -i toolkit   # or grep for the vendor namespace
    php artisan optimize:clear && php artisan list
    

    A command that is present but unregistered looks identical to a command that is broken until you read the service provider's register()/boot().

  4. Read the source. The whole package is under an hour of reading. Write down, in the ticket: the command's exact name, its arguments and options, what it reads/writes/destroys, its exit codes, and whether it is idempotent. This is your first and most important "use case" — the audit.

  5. Decide before you build. The likely outcome of step 4 is that you are looking at 30–80 lines of glue. If so, stop: copy it into app/Console/Commands, drop the dependency, and you have kept the behavior while shedding a five-year-stale version constraint.

First real use case (if it survives): run the command in a sandbox, twice, and diff the output. Non-idempotent commands are the ones that bite in CI and deployment hooks.

For the advertised API patterns — if what you actually want is consistent JSON envelopes, resources, and query helpers — read the source for the ideas, then write them into your own app/ where the team owns them permanently. See Implementation Patterns below. This package is the wrong instrument for that requirement.

Implementation Patterns

Pattern 1: Wrap the command, never call it by name

If you adopt the artisan command, own the name. A project-owned command gives you a stable interface, a place for preconditions and logging, and a one-line removal path.

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SyncSomething extends Command
{
    protected $signature = 'app:sync-something {--dry-run}';
    protected $description = 'Project-owned wrapper; delegates to the vendor command.';

    public function handle(): int
    {
        if ($this->option('dry-run')) {
            $this->info('Dry run: no changes written.');
            return self::SUCCESS;
        }

        $exitCode = $this->call('vendor:command-name', [
            // pass only documented arguments/options
        ]);

        $this->line("delegate exited with {$exitCode}");

        return $exitCode;
    }
}

Everything downstream (CI, deploy hooks, runbooks) calls app:sync-something. When you drop the package you replace one $this->call() with the copied implementation and nothing else changes.

Pattern 2: Scripts branch on exit codes only

if ! php artisan app:sync-something --no-interaction; then
  echo "failed" >&2
  exit 1
fi

Never parse the human-readable output in automation. There is no release history to infer output stability from, and a lone 1.0.0 makes no promises. Exit codes are the contract you can rely on — and only after you have confirmed them in the source.

Pattern 3: The response envelope — write it yourself

This is the advertised capability that most teams actually want, and it is roughly sixty lines in your own codebase. A single macro on the response factory keeps every endpoint consistent:

// app/Providers/AppServiceProvider.php — boot()
Response::macro('api', function (mixed $data = null, int $status = 200, array $meta = []) {
    return response()->json([
        'data'  => $data,
        'meta'  => (object) $meta,
        'error' => null,
    ], $status);
});

Response::macro('apiError', function (string $code, string $message, int $status = 400, array $details = []) {
    return response()->json([
        'data'  => null,
        'meta'  => (object) [],
        'error' => [
            'code'    => $code,
            'message' => $message,
            'details' => (object) $details,
        ],
    ], $status);
});

Cast meta and details to objects so an empty array serializes as {}, not []. Clients that type-check JSON objects will break on [], and you will only find out from a mobile consumer.

Pattern 4: Standardise errors in the exception handler, not per controller

// bootstrap/app.php (Laravel 11+) or app/Exceptions/Handler.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (ValidationException $e, $request) {
        if (! $request->expectsJson()) {
            return null;
        }

        return response()->apiError(
            code: 'validation_failed',
            message: 'The given data was invalid.',
            status: 422,
            details: $e->errors(),
        );
    });
})

Do the same for AuthenticationException (401), AuthorizationException (403), ModelNotFoundException (404), and a catch-all that returns a generic server_error with 500. This is the highest-leverage piece of the whole exercise: it removes try/catch scaffolding from every controller and guarantees a client never sees Laravel's default HTML error page or a raw stack trace.

Pattern 5: Query helpers as a small trait, not a package dependency

Filtering, sorting, and pagination belong in one place. A trait on your base controller or a query-builder macro:

trait SortsAndFilters
{
    protected function applyQuery(Builder $query, Request $request, array $sortable = []): Builder
    {
        if ($sort = $request->query('sort')) {
            $direction = str_starts_with($sort, '-') ? 'desc' : 'asc';
            $column    = ltrim($sort, '-');

            if (in_array($column, $sortable, true)) {
                $query->orderBy($column, $direction);
            }
        }

        return $query;
    }
}

Allow-list the sortable columns. Passing a query param into orderBy() unfiltered is a SQL injection and information-disclosure vector, and it is the most common way "nice API helper" packages get teams into trouble.

Pattern 6: Resources carry the transformation, and eager-load

class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'         => $this->id,
            'name'       => $this->name,
            'email'      => $this->when($request->user()?->isAdmin(), $this->email),
            'team'       => new TeamResource($this->whenLoaded('team')),
            'created_at' => $this->created_at?->toIso8601String(),
        ];
    }
}

whenLoaded turns an N+1 into an omit rather than a silent performance cliff — but it also means a missing with('team') in the controller produces a silently absent field rather than an error. Add a contract test that asserts the field is present for the endpoints where it matters.

Gotchas and Tips

The description and the release note describe two products. Highest-probability source of wasted effort. Budget one source read, record what you found, and do not let the README stand in for a verified capability list. If the package turns out to be console-layer only, treat any "we'll use it for our API envelope" statement in a planning doc as unverified.

The version constraint will become a resolver blocker, not a bug. Five years without a release in a framework-tracking ecosystem means your next Laravel major is where this hurts. Treat it as a scheduled future cost: pin 1.0.0 exactly, and put a calendar note against your next framework upgrade to re-evaluate. Do not adopt a caret range — a lone 1.0.0 gives you nothing to infer from and there is no upgrade path to reason about.

Discovery failures look like breakage. If the command does not appear in php artisan list, run php artisan optimize:clear before you start debugging the package. Then read the service provider. Silent non-registration under a cached bootstrap is a common and confusing first-run symptom.

Check for command namespace collisions. Compare the vendor command's namespace against your existing app/Console/Commands. A duplicate signature means one silently shadows the other, and which one wins depends on registration order.

CI and non-TTY behaviour. Run the command with --no-interaction and confirm it does not block on a prompt. Check the source for --force, --dry-run, or a confirmation guard. If it has none of those and it mutates anything, it does not belong in a deployment hook — that is a separate review with its own sign-off.

Never parse human-readable output. Branch on exit codes only. Capture output for logs, but do not assert against the text.

Your envelope is a public contract from day one. Once a mobile client or a partner integration ships against {data, meta, error}, changing the key names or flipping error from object to string is a breaking API change requiring versioning. Decide the shape once, write it in the ADR, and add a feature test that asserts the full response shape for one success and one failure case per resource. That test is what lets you refactor the controller layer safely later.

Do not leak internals in the error envelope. In production, the catch-all 500 handler should return a fixed server_error message and log the exception with a correlation ID. Gate any details or trace content behind App::hasDebugModeEnabled(). The whole point of a standardised error shape is that it is safe to return unconditionally.

Watch the object-vs-array JSON trap. json_encode([]) is [], not {}. Any client doing schema validation on meta or error.details will break the first time a list is empty. Cast to object at the boundary.

Extension points, in order of preference. (1) Your wrapper command and your own envelope macros — own them, they are yours forever. (2) If the package publishes a config file, publish it in the spike and diff it, so a future upgrade does not silently change defaults. (3) Forking is the last resort; at this size, copying the behavior into app/ is strictly cheaper than a fork you now maintain.

Set the removal cost before you install. It should be one line in composer.json, one wrapper command, and deletion of the copied implementation. If adopting this package ever requires more than that, the evaluation has drifted and you should pull it back to a spike.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo