Takazudo Modular Docs

Type to search...

to open search from anywhere

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

PropertyValue
EndpointGET /api/products
AuthenticationNone (public)
Rate LimitingNone (Netlify default)
Rewrite Rule[[redirects]] in netlify.toml: /api/products -> /.netlify/functions/get-products

Request

Method

GET

Query Parameters

ParameterTypeRequiredDefaultDescription
tagstring (repeatable)NoFilter products by tag. Multiple tags: ?tag=vca&tag=mixer (OR logic)
qstringNoKeyword search across name, subtitle, brand, tags, description (case-insensitive)
pagenumberNo1Page number (1-indexed)
per_pagenumberNo50Items 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

FieldTypeDescription
successbooleanAlways true for successful responses
productsarrayArray of product objects for the current page
totalnumberTotal number of products matching the filter
pagenumberCurrent page number
totalPagesnumberTotal number of pages
availableTagsstring[]All available product tags for filtering UI

Product Object

FieldTypeNullableDescription
slugstringNoUnique product identifier
namestringNoProduct display name
subtitlestringYesProduct subtitle. null if not set
brandstringNoBrand key (e.g., "oxi", "addac", "takazudo")
descriptionstringNoProduct description text
imgSrcstringNoPath to the product image
detailHrefstringYesURL path to the product detail page. null if no detail page exists
tagsstring[]NoArray of tag keys assigned to this product
pricenumberYesPrice in JPY (Japanese Yen). null if not set
specobjectNoProduct specifications
spec.widthstringYesModule width in HP (e.g., "8HP"). null for non-eurorack products
blurhashstringYesBlurhash string for image placeholder. null if metadata unavailable
aspectRationumberNoImage 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

  • page values less than 1 are treated as page 1
  • per_page values greater than 200 are capped at 200
  • Requesting a page beyond totalPages returns an empty products array with the correct total and totalPages

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 utilities
  • src/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
});