GET /api/products
Public product catalog API with tag filtering, keyword search, and pagination
Product API Specification
This document specifies the GET /api/products Netlify serverless function for querying the product catalog with tag filtering and pagination.
Overview
| Property | Value |
|---|---|
| Endpoint | GET /api/products |
| Authentication | None (public) |
| Rate Limiting | None (Netlify default) |
| Rewrite Rule | [[redirects]] in netlify.toml: /api/products -> /.netlify/functions/get-products |
Request
Method
GET
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
tag | string (repeatable) | No | — | Filter products by tag. Multiple tags: ?tag=vca&tag=mixer (OR logic) |
q | string | No | — | Keyword search across name, subtitle, brand, tags, description (case-insensitive) |
page | number | No | 1 | Page number (1-indexed) |
per_page | number | No | 50 | Items per page (max: 200) |
Example Requests
# Get all products (page 1, 50 per page)
GET /api/products
# Filter by single tag
GET /api/products?tag=vca
# Filter by multiple tags (products matching ANY tag)
GET /api/products?tag=vca&tag=mixer
# Pagination
GET /api/products?page=2&per_page=20
# Keyword search
GET /api/products?q=kick
# Combined: keyword + tag + pagination
GET /api/products?q=kick&tag=percussion&page=1&per_page=10
Response
Success Response
Status: 200 OK
{
"success": true,
"products": [
{
"slug": "noisy-kick-v2",
"name": "Kick V2",
"subtitle": "Compact Analog Kick",
"brand": "noisy-fruits-lab",
"description": "4HP analog kick drum module...",
"imgSrc": "/images/p/noisy-kick-v2-0-front",
"detailHref": "/products/kick-v2-intro/",
"tags": ["percussion", "kick", "diy-kit"],
"price": 22000,
"spec": {
"width": "4HP"
},
"blurhash": "UWFiADNGNGj].Tayaxof0ht6s:a}s8a}a}WC",
"aspectRatio": 100
}
],
"total": 175,
"page": 1,
"totalPages": 4,
"availableTags": ["vca", "vco", "mixer", "percussion", "sequencer"]
}
Response Fields
Top Level
| Field | Type | Description |
|---|---|---|
success | boolean | Always true for successful responses |
products | array | Array of product objects for the current page |
total | number | Total number of products matching the filter |
page | number | Current page number |
totalPages | number | Total number of pages |
availableTags | string[] | All available product tags for filtering UI |
Product Object
| Field | Type | Nullable | Description |
|---|---|---|---|
slug | string | No | Unique product identifier |
name | string | No | Product display name |
subtitle | string | Yes | Product subtitle. null if not set |
brand | string | No | Brand key (e.g., "oxi", "addac", "takazudo") |
description | string | No | Product description text |
imgSrc | string | No | Path to the product image |
detailHref | string | Yes | URL path to the product detail page. null if no detail page exists |
tags | string[] | No | Array of tag keys assigned to this product |
price | number | Yes | Price in JPY (Japanese Yen). null if not set |
spec | object | No | Product specifications |
spec.width | string | Yes | Module width in HP (e.g., "8HP"). null for non-eurorack products |
blurhash | string | Yes | Blurhash string for image placeholder. null if metadata unavailable |
aspectRatio | number | No | Image aspect ratio as percentage (e.g., 100 for square). Defaults to 100 |
Sorting
Products are returned in the same order as they appear in product-master-data.mjs, which places newest/featured products first (lowest array index = highest priority).
Error Responses
Error responses follow the existing pattern established by other Netlify functions in the project.
Invalid Method
Status: 405 Method Not Allowed
{
"success": false,
"error": "Method not allowed"
}
Invalid Parameters
Status: 400 Bad Request
{
"success": false,
"error": "Invalid parameters",
"details": [
{ "field": "page", "message": "page must be a positive integer" },
{ "field": "per_page", "message": "per_page must be between 1 and 100" }
]
}
Server Error
Status: 500 Internal Server Error
{
"success": false,
"error": "サーバーエラーが発生しました"
}
CORS
The endpoint follows the same CORS policy as other functions defined in netlify/functions/shared/cors.ts:
- Production:
https://takazudomodular.com - Preview:
https://*--takazudomodular.netlify.app - Local dev:
http://localhost:34434,http://zmod.localhost:34434
Since this is a read-only GET endpoint, CORS preflight is not required for simple requests. The Access-Control-Allow-Methods header includes GET, OPTIONS.
Implementation Notes
Data Source
The function reads from product-master-data.mjs which is bundled with the function at build time. This means:
- No runtime database queries
- Data is a snapshot at deploy time
- Product data updates require redeployment
Blurhash Data
Each product includes a blurhash string and aspectRatio sourced from metadata-db.json at build time. The frontend uses these for blur-up image loading placeholders. If metadata is unavailable for a product, blurhash is null and aspectRatio defaults to 100.
Tag Filtering
When multiple tag parameters are provided, the filter uses OR logic: a product matching ANY of the specified tags is included.
Pagination Behavior
pagevalues less than 1 are treated as page 1per_pagevalues greater than 200 are capped at 200- Requesting a page beyond
totalPagesreturns an emptyproductsarray with the correcttotalandtotalPages
Available Tags
The availableTags field returns all tags that exist across the full product catalog, regardless of the current filter. This enables the frontend to show all filter options at all times.
Function File Location
netlify/functions/get-products.ts
Dependencies
netlify/functions/shared/cors.ts— CORS utilitiessrc/data/product-master-data.mjs— Product catalog data (bundled at build time)
Netlify Rewrite
Add to netlify.toml:
[[redirects]]
from = "/api/products"
to = "/.netlify/functions/get-products"
status = 200
Usage Examples
Fetch All Products (JavaScript)
const response = await fetch('/api/products');
const data = await response.json();
// data.products contains first 50 products
// data.total contains total count
Filter by Tag
const response = await fetch('/api/products?tag=vca&tag=mixer');
const data = await response.json();
// data.products contains products tagged with EITHER "vca" OR "mixer"
Paginated Fetch
async function fetchAllProducts() {
const allProducts = [];
let page = 1;
let totalPages = 1;
do {
const response = await fetch(`/api/products?page=${page}&per_page=50`);
const data = await response.json();
allProducts.push(...data.products);
totalPages = data.totalPages;
page++;
} while (page <= totalPages);
return allProducts;
}
Build a Tag Filter UI
const response = await fetch('/api/products');
const data = await response.json();
// data.availableTags contains all possible tags
// Use these to build filter checkboxes/buttons
data.availableTags.forEach(tag => {
// Create filter UI element for each tag
});