# Shopify

Lightweight, dependency-free, in-memory fake of the Shopify Admin REST API (`2024-01`) for testing code that talks to Shopify.

Default port: `4758`

## Quick start

```js
import { ShopifyServer } from "./services/shopify/src/server.js";

const server = new ShopifyServer(4758);
await server.start();
// ... run your app/tests ...
await server.stop();
```

Point your client at the fake and use any access token:

```js
const res = await fetch("http://127.0.0.1:4758/admin/api/2024-01/products.json", {
  method: "POST",
  headers: {
    "X-Shopify-Access-Token": "shpat_parlel",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ product: { title: "Snowboard", vendor: "Parlel" } }),
});
// => 201 { product: { id, title, created_at, ... } }
```

## Implemented operations

All `/admin/api/2024-01/*` routes require an `X-Shopify-Access-Token` header
(any non-empty token, or `Authorization: Basic` for private apps). Resources are
wrapped under the singular key on read/write and the plural key on list. State
is in-memory and ephemeral.

### Products — `/admin/api/2024-01/products.json`

- `GET /products.json` — list (`{ products: [...] }`).
- `POST /products.json` — create (`201 { product: {...} }`), body `{ product: {...} }`.
- `GET /products/:id.json` — retrieve.
- `PUT /products/:id.json` — update.
- `DELETE /products/:id.json` — delete (`200 {}`).

### Orders — `/admin/api/2024-01/orders.json`

- `GET` / `POST` / `GET :id` / `PUT :id` / `DELETE :id` — same CRUD shape, wrapped in `{ order }` / `{ orders }`.

### Customers — `/admin/api/2024-01/customers.json`

- `GET` / `POST` / `GET :id` / `PUT :id` / `DELETE :id` — wrapped in `{ customer }` / `{ customers }`.

### Shop — `/admin/api/2024-01/shop.json`

- `GET /shop.json` — returns store metadata (`{ shop: {...} }`).

### Service & inspection operations (parlel extensions)

- `GET /` — service metadata.
- `GET /health` — health check (`{ status: "ok" }`).
- `POST /__parlel/reset` — reset all in-memory state.
- `OPTIONS *` — CORS preflight (`204`).

## Access via MCP / preview URL

Inside a parlel sandbox the service is reachable at its preview URL
(`SHOPIFY_BASE_URL`, e.g. `http://127.0.0.1:4758`). Point your Admin REST client
at that host and pass any `X-Shopify-Access-Token`. MCP agents can call any
documented endpoint; `/__parlel/reset` clears state between scenarios.

## Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

| Feature | Status |
| --- | --- |
| `products` CRUD | ✅ Supported |
| `orders` CRUD | ✅ Supported |
| `customers` CRUD | ✅ Supported |
| `shop.json` | ✅ Supported |
| `{ resource: {...} }` wrap / plural list shape | ✅ Supported |
| Numeric resource ids + `admin_graphql_api_id` | ✅ Supported |
| GraphQL Admin API | ⟳ Roadmap — REST only |
| Webhooks / fulfillment / inventory / metafields | ⟳ Roadmap |
| Variants/images side-effects, price rules | ◐ Stored as-is, not computed |
| Rate limiting / leaky bucket (`429`) | ✓ By design — Never throttles — local tests run at full speed, zero cost |
| Token validity / scopes | ✓ By design — Any non-empty credential is accepted — no real secrets needed |

## Error codes & shapes

```json
{ "errors": "Not Found" }
```

| Status | When |
| --- | --- |
| `400` | malformed JSON body |
| `401` | missing `X-Shopify-Access-Token` |
| `404` | unknown id or endpoint |
| `405` | method not allowed |

## Manifest

See `services/shopify/manifest.json`:

- name: `shopify`, image: `parlel/shopify:1.0`
- port: `4758`, protocol: `http`, healthcheck: `/health`, startup ≈ 100ms
- env: `SHOPIFY_ACCESS_TOKEN`, `SHOPIFY_SHOP`, `SHOPIFY_BASE_URL`
