Build a shop
Two resources, one relationship between them, and images that are resized and compressed as they arrive. Six commands from an empty app to a dashboard you can add products in.
1. Create the app
Section titled “1. Create the app”pnpm create nevela my-shopcd my-shop2. Categories
Section titled “2. Categories”php nevela resource Category --fields="name:string, slug:slug!, image:image?" --icon=tags --seed=8image:image? is an optional picture. --seed=8 creates the table and fills it with eight categories, most of them with a placeholder image, so there is something to look at.
3. Products
Section titled “3. Products”php nevela resource Product --fields="name:string, sku:string!, price:money, image:image(product)?, category:belongsTo(Category), active:boolean" --icon=package --seed=40Two fields here are new:
category:belongsTo(Category)links each product to a category. It is stored ascategory_id, and the API calls itcategoryId.image:image(product)is a picture optimised with theproductprofile: it fits inside 1000×1000, with a 300×300 thumbnail and a 600×600 card made alongside.
A product has to point at a category, so categories are created first. Asking for a relation to a resource that does not exist yet is refused, with the command to create it.
4. Run it
Section titled “4. Run it”php nevela devSign in at the address it prints. In the dashboard:
- Products shows each product’s thumbnail and the name of its category.
- New product has a picker that searches categories, and a box to drop an image on.
- A category’s page lists the products in it.
- Deleting a category that still has products is refused: “8 products belong to this category. Move or delete them first.”
What happened to the image
Section titled “What happened to the image”Drop a 4000×3000 photo from a phone on the product form and this is stored:
| File | Size | What it is for |
|---|---|---|
…-kettle.webp |
1000×750, about 85 KB | the product page |
…-kettle.card.webp |
600×450, about 48 KB | a grid of products |
…-kettle.thumb.webp |
300×300, about 22 KB | a table row, a cart line |
| the original, 1.2 MB | kept privately | optimising again later with other settings |
The photo was also turned the right way up, and the location it was taken at was removed with the rest of its metadata. Files and images has the details and how to change the sizes.
The API for a storefront
Section titled “The API for a storefront”The same API the dashboard uses serves a storefront. A product comes back with its image’s addresses and dimensions beside the key:
{ "id": "01a10804-8b62-7051-9637-61f4872386fa", "name": "Smart Kettle", "price": 49.5, "categoryId": "01a10804-7b04-72d6-aabf-a2113b9b6bc5", "image": "products/image/2026/10/778e5b3c-…-kettle.webp", "imageFile": { "url": "https://api.example.com/api/_nevela/files/products/image/2026/10/778e5b3c-…-kettle.webp", "width": 1000, "height": 750, "renditions": { "thumb": { "url": "…-kettle.thumb.webp", "width": 300, "height": 300 }, "card": { "url": "…-kettle.card.webp", "width": 600, "height": 450 } } }}Products in one category:
GET /api/products?filter[categoryId]=01a10804-7b04-72d6-aabf-a2113b9b6bc5&sort=nameIn a page, use the dimensions so nothing moves when the picture arrives, and let the browser pick a size:
{product.imageFile && ( <img src={product.imageFile.renditions.card.url} srcSet={`${product.imageFile.renditions.card.url} 600w, ${product.imageFile.url} 1000w`} sizes="(max-width: 640px) 50vw, 300px" width={product.imageFile.renditions.card.width} height={product.imageFile.renditions.card.height} alt={product.name} loading="lazy" />)}The image is optional here, so imageFile is null for a product without one.
Every Nevela route needs a signed-in user, and a policy cannot change that: the request is turned away before the policy is asked. For a storefront, either call the API from the storefront’s server with a token, or add read-only routes of your own in routes/api.php, which is outside Nevela’s signed-in group.
- Relationships: optional links, deleting, and the Eloquent methods you get.
- Files and images: profiles, formats, limits and where files are kept.