# 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 ```sh npm install npm start # node server.js ``` Open . API docs: . ```sh npm run dev # node --watch server.js npm run bun # bun server.js (also runs on Node) ``` ## Schema ```js 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: ```json { "error": "Name is required.", "fields": { "name": "Name is required." } } ``` ## Examples ```sh 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` ```json [ { "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` ```json { "error": "ID must be a whole number, like 1." } ``` ```json { "error": "No menu item with ID 99." } ```