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

Dystore Api Laravel Package

dystcz/dystore-api

JSON:API layer for the Dystore headless commerce stack on Laravel. Exposes products, carts, orders, customers, and checkout as JSON:API endpoints built on Lunar, letting any storefront, SPA, or mobile app run off a single Laravel backend.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites. You need a working Lunar install (the headless e-commerce core) on Laravel 10+/11+. Dystore-API is a thin JSON:API layer on top of Lunar — it does not ship its own product/cart/order tables, so if php artisan lunar:install hasn’t run yet, do that first.

Install.

composer require dystcz/dystore-api
php artisan vendor:publish --tag=dystore-api-config

Then check config/dystore.php (or config/dystore-api.php depending on version) — this is the single most important file. It controls:

  • route prefix (/api/v1 by default) and middleware stack
  • auth guard / token driver (Sanctum)
  • which JSON:API server is registered and which resource types are exposed
  • pagination defaults and allowed include/filter/sort paths

Run it and hit it.

php artisan route:list --path=api
curl -H "Accept: application/vnd.api+json" https://your.app/api/v1/products

If that returns a JSON:API document with data, links, meta, you’re wired correctly.

First use case to prove the stack. Do the shallowest end-to-end flow before touching anything custom:

  1. GET /api/v1/products?include=variants,thumbnail — confirm sparse fieldsets + includes work.
  2. POST /api/v1/carts — create a guest cart, note the returned cart_id.
  3. POST /api/v1/carts/{id}/cart-lines — add a purchasable (variant) with quantity.
  4. POST /api/v1/checkout (or /orders depending on version) with a payment/address payload.

If that loop works, everything else is composition.

Where to look first in the source. Read in this order: config/ → routes/ → src/JsonApi/ (the server + resource classes) → src/Http/Controllers/. The resource classes are the contract; the controllers are boring glue.


Implementation Patterns

Treat it as a JSON:API server, not a REST API

Dystore-API uses laravel-json-api under the hood. Every endpoint follows the JSON:API 1.0 spec: data/included/relationships, filter[...], sort=, page[number], fields[type]=, and include=. Front-end clients should use a JSON:API client (e.g. json-api-client, axios wrappers, or generated SDKs) rather than hand-rolling request code.

GET /api/v1/products
  ?filter[status]=published
  &filter[price][between]=10,50
  &include=variants,thumbnail,defaultUrl
  &fields[products]=name,slug,price
  &sort=-created_at
  &page[size]=24
Accept: application/vnd.api+json

Auth flow

Sanctum tokens, standard Laravel. Register/login return a token; put it in Authorization: Bearer …. Guest carts don’t require auth — capture the cart ID from POST /carts and pass it forward. Associate the cart to a customer with PATCH /carts/{id} once the user logs in (or use a cart-merge endpoint if your version ships one).

Typical frontend workflow (Next.js / Nuxt / SPA)

  1. Bootstrapping — fetch /products and /product-variants with includes; use fields[] to keep payloads small.
  2. Cart — cart state lives server-side; the client stores only the cart ID. Every mutation (cart-lines, cart-line-quantities) returns the full cart document so you can re-render from the response rather than re-fetching.
  3. Checkout — the checkout endpoint is multi-step: address → shipping option → payment → place order. Each step is a separate POST that returns the updated order/checkout document. Do not try to POST a single composite payload; follow the state machine the API exposes.
  4. Orders — after auth, GET /orders filtered by customer scope. Use relationships (order-lines, transactions) via includes for order-history pages.

Extending without forking

The package is designed for extension, not modification:

// app/JsonApi/V1/Products/ProductResource.php
namespace App\JsonApi\V1\Products;

use Dystcz\DystoreApi\JsonApi\V1\Products\ProductResource as BaseResource;

class ProductResource extends BaseResource
{
    public function attributes($request): iterable
    {
        yield from parent::attributes($request);
        yield 'my_custom_flag' => fn () => $this->is_featured;
    }
}

Register your override in the config’s resource map (or the server class returned by the config), and the base resource is swapped everywhere — routes, includes, related resources all pick it up.

Custom endpoints

If you need non-JSON:API behavior (webhooks, PDF invoices, admin bulk ops), don’t hijack the JSON:API server. Add a normal Laravel controller in app/Http/Controllers/Api/ and register routes in routes/api.php. Keep those outside the /api/v1/jsonapi namespace so clients know the difference.

Testing patterns

Use Laravel’s HTTP assertions plus JSON:API shape checks. Laravel-json-api ships assertJsonApiDocument style helpers via its testing trait — assert on data.type, data.id, and relationship linkage rather than exact JSON, since the package can add attributes between minor versions.

$this->jsonApi()
    ->expects('products')
    ->get('/api/v1/products')
    ->assertFetchedMany();

If you use Pest, wrap these in a shared api() helper that sets the correct Accept and Content-Type headers automatically — otherwise half your tests will mysteriously 406.


Gotchas and Tips

The Accept header is load-bearing

laravel-json-api returns 406 Not Acceptable for requests without Accept: application/vnd.api+json, and 415 for write requests without Content-Type: application/vnd.api+json. This is the single most common “it doesn’t work” complaint. Set both defaults in your HTTP client:

axios.defaults.headers.common['Accept'] = 'application/vnd.api+json';
axios.defaults.headers.common['Content-Type'] = 'application/vnd.api+json';

Includes must be whitelisted

?include=variants.prices only works if the resource class declares variants and the related resource declares prices in getIncludes(). If an include silently disappears, it’s almost always a missing allowedIncludes() / getIncludes() entry on the resource, not a route problem. Grep the base resource classes — don’t guess.

Version-pin Lunar and Dystore together

Dystore-API tracks Lunar’s schema and model API. Upgrading Lunar without upgrading Dystore-API (or vice versa) is the fastest way to get Method does not exist and column-not-found errors on otherwise-valid queries. Pin both in composer.json with a constraint that reflects this coupling, and read the Dystore changelog before every Lunar bump.

N+1 hiding inside relationships

JSON:API relationship links are cheap, but relationship data is lazy-loaded. A product index with include=variants.prices on 50 products will hammer the DB unless the resource eager-loads. Lunar’s repositories already do some of this, but if you override a resource or query, add ->with([...]) explicitly. Use Laravel Debugbar or DB::listen on the index endpoint before going to production.

Sparse fieldsets are a performance tool — use them

fields[products]=name,slug,price cuts payload size by 60–80% on real catalogues. But fields[] applies to both the primary type and included types — you must specify fields[variants] too or you’ll get default (full) variant documents. Front-ends that ignore fields[] are the usual cause of slow first paint.

Route prefix and middleware are configurable, but be careful

Changing route.prefix is safe. Adding middleware (e.g. a throttle:api or a tenant resolver) needs to be added to both the JSON:API server middleware and any custom checkout routes you add — they’re separate stacks. Put shared middleware in the config so you don’t forget one.

Checkout is a state machine, not a single POST

Attempting to POST /checkout with address + payment + shipping in one payload will fail validation because each step depends on the previous. Read src/Actions/Checkout/ (or the equivalent CheckoutController) before you write front-end checkout code — it tells you exactly which transitions are legal and which fields each step demands. This will save you a day.

Debugging JSON:API errors

Laravel-json-api wraps exceptions into JSON:API errors — the underlying exception message goes into error detail only when APP_DEBUG=true. In production, detail is suppressed; log the exception server-side and correlate by request ID. For local debugging, temporarily set JSONAPI_DEBUG=true (if your version exposes it) or dump $exception->getMessage() in a listener rather than in the resource.

Extension points to remember

  • Resources — override any base resource class and swap via config.
  • Schema / field definitions — the resource’s fields() and relationships() are the contract; add attributes here, not in controllers.
  • Actions — checkout and cart operations live in action classes; override via container binding if you need different business logic (e.g. custom tax at checkout).
  • Serialisers / transformers — if you need a different meta shape, override the server’s jsonApi() encoder settings.
  • Events — Dystore fires normal Lunar events (CartUpdated, OrderPlaced, etc.); hook into those rather than adding listeners inside the package.

Watch the “READ ONLY” marker

The GitHub description noting [READ ONLY] means the repo is split or mirrored from a bigger monorepo. Issues and PRs may belong upstream (likely the dystcz/dystore umbrella). Don’t open a PR against a mirror — check the README’s contributing link first. This also means the changelog can lag by a few days behind actual tags; inspect the CHANGELOG.md in the tree as the source of truth.

Testing without hitting a real payment provider

Lunar’s payment drivers are pluggable. During development, bind a fake or test-mode driver in AppServiceProvider so the checkout endpoint doesn’t reach Stripe/PayPal. The Dystore test suite itself does this — copy the pattern from tests/ in the repo if you’re unsure where to hook it.

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.
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
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource