main
Meal API
A food menu API with a single-page frontend. A Meal is one item on the menu; the
collection is /api/menu, validated with zod and documented at /openapi.
Running
npm install
npm start # node server.js
Open http://localhost:3000. API docs: http://localhost:3000/openapi.
npm run dev # node --watch server.js
npm run bun # bun server.js (also runs on Node)
Schema
const MEAL_CATEGORIES = ['appetizer', 'main', 'dessert', 'drink']
const mealDraftSchema = z.object({
name: z.string().trim().min(1, 'Name is required.').max(80, 'Keep the name under 80 characters.'),
category: z.enum(MEAL_CATEGORIES, { error: 'Pick a category: …' }),
price: z.number({ error: 'Price is required.' })
.nonnegative('Price cannot be negative.')
.refine((v) => Math.round(v * 100) / 100 === v, 'Price can have at most 2 decimal places.'),
description: z.string().trim().max(240, '…').optional(),
available: z.boolean({ error: 'Available must be true or false.' }).optional(),
})
The same schema validates the request, types the handler and generates the OpenAPI spec.
| Field | Type | Notes |
|---|---|---|
id |
integer ≥ 1 | assigned by the server |
name |
string, 1–80 chars | trimmed |
category |
enum | appetizer · main · dessert · drink |
price |
number ≥ 0 | at most 2 decimal places |
description |
string, ≤ 240 chars | defaults to "" |
available |
boolean | defaults to true |
createdAt |
ISO datetime | set on create |
updatedAt |
ISO datetime | bumped on every update |
Endpoints
| Method | Path | Result |
|---|---|---|
| GET | / |
the frontend |
| GET | /openapi |
interactive API docs |
| GET | /openapi/json |
the OpenAPI spec |
| GET | /api/menu |
list meals |
| GET | /api/menu/:id |
one meal |
| POST | /api/menu |
create a meal → 201 |
| PUT | /api/menu/:id |
replace a meal |
| PATCH | /api/menu/:id |
update only the fields you send |
| DELETE | /api/menu/:id |
remove a meal → 204 |
GET /api/menu also accepts category, available, search, sort (name, price,
createdAt) and order (asc, desc).
Errors come back as a message plus the offending fields:
{ "error": "Name is required.", "fields": { "name": "Name is required." } }
Examples
curl http://localhost:3000/api/menu
curl "http://localhost:3000/api/menu?category=main&sort=price&order=desc"
curl "http://localhost:3000/api/menu?search=adobo"
curl http://localhost:3000/api/menu/2
curl -X POST http://localhost:3000/api/menu \
-H 'content-type: application/json' \
-d '{"name":"Halo-Halo","category":"dessert","price":65.5}'
curl -X PATCH http://localhost:3000/api/menu/2 \
-H 'content-type: application/json' \
-d '{"price":55}'
curl -X DELETE http://localhost:3000/api/menu/2
GET /api/menu → 200
[
{
"id": 1,
"name": "Adobo",
"category": "main",
"price": 75,
"description": "Pork belly simmered in soy sauce, vinegar and bay leaves.",
"available": true,
"createdAt": "2026-09-16T12:00:00.000Z",
"updatedAt": "2026-09-16T12:00:00.000Z"
}
]
GET /api/menu/abc → 400 · GET /api/menu/99 → 404
{ "error": "ID must be a whole number, like 1." }
{ "error": "No menu item with ID 99." }
Languages
HTML
70.9%
JavaScript
29.1%