The Cooklang Store API provides RESTful endpoints for managing recipes stored in a git repository. All recipes are stored as .cook files and tracked in git for version history and collaboration.
Key Design: Recipe names (titles) are derived from Cooklang YAML front matter metadata, not provided by the client. File names on disk are automatically generated from recipe titles and kept in sync.
- Current Version: v1
- Base URL:
/api/v1
Used when retrieving individual recipes or after create/update operations.
{
"recipeId": "a1b2c3d4e5f6",
"recipeName": "Chocolate Cake",
"path": "desserts",
"fileName": "chocolate-cake.cook",
"description": null,
"content": "---\ntitle: Chocolate Cake\n---\n\n# Recipe content..."
}Notes:
recipeNameis derived from thetitlefield in YAML front matterfileNameis generated from the recipe name (lowercase, spaces→hyphens,.cookextension)pathrepresents the directory location (relative to data-dir, norecipes/prefix)descriptionis omitted from JSON if null (usingskip_serializing_if)contentalways includes YAML front matter with title
Used in list and search endpoints.
{
"recipeId": "a1b2c3d4e5f6",
"recipeName": "Chocolate Cake",
"path": "desserts"
}Notes:
- No
fileNameorcontentin summaries pathomitted from JSON if null
{
"error": "error_code",
"message": "Human-readable error message",
"details": {
"field": "Additional context"
}
}All recipe content must include YAML front matter with a title field at the start:
---
title: Chocolate Cake
---
# Instructions
@flour{2%cups}
@sugar{1%cup}Format:
- Delimited by
---on its own lines (start and end) - Must contain at least
title: Recipe Name - Can include additional metadata fields
Validation:
- Create and update operations validate that content includes YAML front matter with
titlefield - Missing title → 400 Bad Request
- URL:
/health - Method:
GET - Description: Simple health check endpoint
- Response:
OK(plain text) - Status Code:
200 OK
- URL:
/api/v1/status - Method:
GET - Description: Get server status and recipe statistics
- Response:
{ "status": "running", "version": "0.1.0", "recipe_count": 42, "categories": 8 }
- URL:
/api/v1/recipes - Method:
POST - Content-Type:
application/json - Request Body:
{ "content": "---\ntitle: Chocolate Cake\n---\n\n@flour{2%cups}...", "path": "desserts", "author": "Alice", "comment": "Classic recipe from grandma" }content(required): Recipe in Cooklang format, must include YAML front matter withtitlepath(optional): Directory path for organization (defaults to root if omitted)author(optional): Author name for git commitcomment(optional): Commit message
- Response:
{ "recipeId": "a1b2c3d4e5f6", "recipeName": "Chocolate Cake", "path": "desserts", "fileName": "chocolate-cake.cook", "description": null, "content": "---\ntitle: Chocolate Cake\n---\n\n@flour{2%cups}..." } - Status Code:
201 Created - Validation:
contentis required and cannot be emptycontentmust include valid YAML front matter withtitlefield- Missing title → 400 Bad Request
- URL:
/api/v1/recipes - Method:
GET - Query Parameters:
limit(optional): Items per page (default: 20, max: 100)offset(optional): Items to skip (default: 0)
- Response:
{ "recipes": [ { "recipeId": "a1b2c3d4e5f6", "recipeName": "Chocolate Cake", "path": "desserts" } ], "pagination": { "limit": 20, "offset": 0, "total": 42 } } - Status Code:
200 OK
- URL:
/api/v1/recipes/search - Method:
GET - Query Parameters:
q(required): Search query (case-insensitive substring match on recipe name)limit(optional): Items per page (default: 20, max: 100)offset(optional): Items to skip (default: 0)
- Response: Same as List Recipes (array of RecipeSummary)
- Status Code:
200 OK - Validation:
qcannot be empty
- URL:
/api/v1/recipes/{recipe_id} - Method:
GET - Path Parameters:
recipe_id(required): Unique recipe identifier (12-character hex string)
- Response: Full RecipeResponse with all fields and content
- Status Code:
200 OK - Error Codes:
404 Not Found: Recipe not found
- URL:
/api/v1/recipes/{recipe_id} - Method:
PUT - Content-Type:
application/json - Path Parameters:
recipe_id(required): Unique recipe identifier
- Request Body (at least one field required):
{ "content": "---\ntitle: Dark Chocolate Cake\n---\n\n...", "path": "desserts", "author": "Bob", "comment": "Updated ingredients" }content(optional): New recipe content. If provided, must include YAML front matter withtitlefieldpath(optional): New directory path. If provided, recipe is moved to this locationauthor(optional): Author name for git commitcomment(optional): Commit message
- Response: Full updated RecipeResponse
- Status Code:
200 OK - File Renaming: If recipe content is updated and the title changes, the file on disk is automatically renamed to match the new recipe name
- Error Codes:
404 Not Found: Recipe not found400 Bad Request: No fields provided, or content provided but missing YAML front matter with title
- URL:
/api/v1/recipes/{recipe_id} - Method:
DELETE - Path Parameters:
recipe_id(required): Unique recipe identifier
- Response: Empty body
- Status Code:
204 No Content - Error Codes:
404 Not Found: Recipe not found
These endpoints help clients find recipes when recipe IDs change due to rename operations.
- URL:
/api/v1/recipes/find-by-name - Method:
GET - Query Parameters:
q(required): Recipe name search term (case-insensitive substring match)limit(optional): Items per page (default: 20, max: 100)offset(optional): Items to skip (default: 0)
- Description: Search for recipes by name. Use this when a recipe ID has changed due to a rename.
- Response: Array of RecipeSummary
{ "recipes": [ { "recipeId": "a1b2c3d4e5f6", "recipeName": "Chocolate Cake", "path": "desserts" } ], "pagination": { "limit": 20, "offset": 0, "total": 1 } } - Status Code:
200 OK(returns empty array if no matches, not 404)
- URL:
/api/v1/recipes/find-by-path - Method:
GET - Query Parameters:
path(required): Recipe directory path (relative to data-dir, norecipes/prefix)
- Description: Find a recipe at a specific path. Use this when you know the location but not the recipe ID.
- Response: Single RecipeSummary (wrapped in object)
{ "recipe": { "recipeId": "a1b2c3d4e5f6", "recipeName": "Chocolate Cake", "path": "desserts" } } - Status Code:
200 OK - Error Codes:
404 Not Found: Recipe at that path not found
- URL:
/api/v1/categories - Method:
GET - Response:
{ "categories": ["appetizers", "desserts", "mains", "sides"] } - Status Code:
200 OK
- URL:
/api/v1/categories/{name} - Method:
GET - Path Parameters:
name(required): Category name (supports hierarchical paths with/separators)
- Description: Categories can be hierarchical, reflecting the directory structure. Use URL encoding for
/as%2F. - Examples:
/api/v1/categories/desserts- Get all recipes in thedessertsdirectory/api/v1/categories/meals%2Fmeat%2Ftraditional- Get all recipes inmeals/meat/traditional
- Response:
{ "category": "desserts", "count": 12, "recipes": [ { "recipeId": "a1b2c3d4e5f6", "recipeName": "Chocolate Cake", "path": "desserts" } ] } - Status Code:
200 OK - Error Codes:
404 Not Found: Category not found
Important: Recipe IDs are derived from the recipe's file path (git_path) using a SHA256 hash. When a recipe is renamed (due to title change), its ID will change.
- IDs are stable across content edits (same file = same path = same ID)
- IDs change on rename (title change triggers automatic file rename on disk)
- IDs are deterministic (same path always produces same ID)
If a bookmarked recipe ID returns 404:
- Use
GET /api/v1/recipes/find-by-name?q=recipe-nameto search by name - Use
GET /api/v1/recipes/find-by-path?path=category/nameif you know the path - Clients should not rely on recipe IDs as permanent identifiers
File names are automatically generated from recipe titles using these rules:
- Convert to lowercase
- Replace spaces with hyphens
- Remove special characters (keep only alphanumeric and hyphens)
- Append
.cookextension
Examples:
- "Chocolate Cake" →
chocolate-cake.cook - "Pasta Carbonara (Italian)" →
pasta-carbonara-italian.cook - "Sweet & Sour" →
sweet-sour.cook
File names are kept synchronized with recipe titles. When you update a recipe's title, its file name is automatically updated on disk.
Pagination is supported on list and search endpoints:
limit: Number of items to return (capped at 100, default 20)offset: Number of items to skip (default 0)total: Total number of items available (in response)
Example: /api/v1/recipes?limit=10&offset=20
All errors return appropriate HTTP status codes:
200 OK: Successful GET request201 Created: Successful POST (resource created)204 No Content: Successful DELETE400 Bad Request: Invalid input or validation failure404 Not Found: Resource not found500 Internal Server Error: Server error
Error responses include:
error: Machine-readable error codemessage: Human-readable error descriptiondetails(optional): Additional context about the error
- Content-Type:
application/json - Character Encoding: UTF-8
- Max Body Size: 10MB (for recipe content)
The API has CORS enabled with permissive policy to allow requests from any origin.
The repository includes test fixtures in tests/fixtures/ with complete Cooklang recipes for API testing:
pasta.cook,chocolate-cake.cook,pad-thai.cook,chicken-biryani.cook, and more- All fixtures have YAML front matter with
titlefield - Ready to use in API requests via curl or Postman
curl -X POST http://localhost:3000/api/v1/recipes \
-H "Content-Type: application/json" \
-d '{
"content": "---\ntitle: Pasta Carbonara\n---\n\n@eggs{4} @bacon{200%g} @pasta{400%g}",
"path": "mains",
"author": "Chef Alice"
}'Response (201 Created):
{
"recipeId": "a1b2c3d4e5f6",
"recipeName": "Pasta Carbonara",
"path": "mains",
"fileName": "pasta-carbonara.cook",
"content": "---\ntitle: Pasta Carbonara\n---\n\n@eggs{4} @bacon{200%g} @pasta{400%g}"
}curl "http://localhost:3000/api/v1/recipes/search?q=chocolate&limit=10"Response (200 OK):
{
"recipes": [
{
"recipeId": "a1b2c3d4e5f6",
"recipeName": "Chocolate Cake",
"path": "desserts"
}
],
"pagination": {
"limit": 10,
"offset": 0,
"total": 1
}
}curl http://localhost:3000/api/v1/recipes/a1b2c3d4e5f6curl -X PUT http://localhost:3000/api/v1/recipes/a1b2c3d4e5f6 \
-H "Content-Type: application/json" \
-d '{
"content": "---\ntitle: Dark Chocolate Cake\n---\n\n# New instructions..."
}'Note: Recipe file on disk will be renamed from chocolate-cake.cook to dark-chocolate-cake.cook.
If the recipe ID has changed due to a rename:
curl "http://localhost:3000/api/v1/recipes/find-by-name?q=Dark%20Chocolate"curl "http://localhost:3000/api/v1/recipes/find-by-path?path=desserts"curl http://localhost:3000/api/v1/categoriescurl http://localhost:3000/api/v1/categories/mainsCurrently, the API does not require authentication. This is planned for a future phase.
Rate limiting is not currently implemented. This is planned for a future phase.
The API follows semantic versioning:
- Current:
/api/v1 - Future versions would be available at
/api/v2, etc.
The version is also included in the status endpoint response.