Restaurant menus look simple on the frontend.
A category name, an item name, a description, maybe a price and an image.
But once you try to build a menu website that supports search, filtering, nutrition data, multiple sizes, location-specific prices, and regular updates, the data model becomes much more interesting.
Recently I was thinking about how I would structure a restaurant menu dataset so that the frontend remains simple even as the amount of data grows.
Here's one approach.
Don't Start With the UI
It's tempting to start with cards like this:
```html id="ms4h2k"
Chicken Biscuit
Chicken served on a biscuit
$5.99
That works for three items.
It becomes painful when you have hundreds.
Instead, I prefer to treat the menu as structured data first.
```javascript id="sj8dp1"
const item = {
id: 101,
name: "Chicken Biscuit",
category: "breakfast",
description: "Chicken served on a biscuit",
price: 5.99
};
Now the UI is simply a representation of the data.
That small architectural decision makes future features much easier.
Start With Categories
Most restaurant menus have logical groups.
For example:
```javascript id="sk8h3e"
const categories = [
{
id: "breakfast",
name: "Breakfast"
},
{
id: "chicken",
name: "Chicken"
},
{
id: "sandwiches",
name: "Sandwiches"
},
{
id: "sides",
name: "Sides"
},
{
id: "drinks",
name: "Beverages"
}
];
Each menu item can then reference a category ID.
```javascript id="b7wx2p"
const menuItems = [
{
id: 1,
categoryId: "breakfast",
name: "Chicken Biscuit",
price: 5.99
},
{
id: 2,
categoryId: "sandwiches",
name: "Chicken Sandwich",
price: 6.49
}
];
This is much cleaner than duplicating category metadata inside every item.
Real Menus Need More Than One Price
One complication appears quickly:
Not every menu item has one universal price.
You might have:
```text id="zn1pf4"
Regular
Large
Single
Combo
Pickup
Delivery
So this:
```javascript id="qq8x5v"
price: 5.99
may eventually become:
```javascript id="rn6az2"
prices: {
item: 5.99,
combo: 8.49
}
Or, for more flexibility:
```javascript id="dw4gp3"
prices: [
{
type: "single",
amount: 5.99
},
{
type: "combo",
amount: 8.49
}
]
The second approach is slightly more verbose but easier to extend.
Location-Based Pricing Changes Everything
Restaurant prices can vary by location.
If your application needs to support that, avoid treating price as an immutable property of the menu item.
A better model might separate products from location pricing.
```javascript id="kc6sy2"
const product = {
id: 101,
name: "Chicken Biscuit",
categoryId: "breakfast"
};
Then:
```javascript id="ts2gq8"
const locationPrice = {
productId: 101,
locationId: 25,
amount: 5.99,
currency: "USD"
};
Now the same product can have different prices at different restaurants without duplicating the entire item.
Conceptually:
```text id="av3qh5"
Product
|
+------ Location A -> $5.99
|
+------ Location B -> $6.19
|
+------ Location C -> $6.39
This becomes important surprisingly quickly.
## Model Availability Separately
Another mistake is assuming every item is available everywhere.
Instead of:
```javascript id="fz9nq1"
available: true
you may eventually need:
```javascript id="hm7ty3"
availability: {
locationId: 25,
productId: 101,
available: true
}
Why?
Because a menu item might be:
* available nationally
* unavailable at one restaurant
* temporarily unavailable
* seasonal
* breakfast-only
* location-specific
Availability is really its own piece of data.
## What About Breakfast Hours?
This creates another interesting modeling problem.
Suppose a product belongs to breakfast.
You could write:
```javascript id="vr5cj4"
{
id: 101,
name: "Breakfast Biscuit",
availableFrom: "05:00",
availableUntil: "10:30"
}
But that assumes every location follows the same schedule.
A more flexible model is:
```javascript id="fm8h21"
const schedule = {
locationId: 25,
categoryId: "breakfast",
monday: {
start: "05:00",
end: "22:00"
}
};
Now restaurant schedules can change independently of product information.
That's a much better separation of concerns.
## Keep Nutrition Data Structured
If you plan to add nutrition information, don't bury it inside the description.
Avoid:
```javascript id="jx7z30"
description:
"Chicken biscuit with 620 calories and 32g fat"
Use:
```javascript id="pg2x91"
nutrition: {
calories: 620,
fat_g: 32,
carbs_g: 48,
protein_g: 24,
sodium_mg: 1350
}
Now you can build features like:
```text id="d8hm22"
Show items under 500 calories
Sort by protein
Filter by sodium
Compare two items
without parsing text.
Structured data is almost always easier to work with later.
Allergens Should Also Be Data
The same principle applies to allergens.
```javascript id="cm7qd5"
allergens: [
"wheat",
"milk",
"egg"
]
Then the frontend can render:
```javascript id="kp5f3s"
function renderAllergens(item) {
return item.allergens.join(", ");
}
You can also implement filtering:
```javascript id="u2w9rc"
const withoutMilk = menuItems.filter(
item => !item.allergens.includes("milk")
);
For a real restaurant application, allergen handling requires much more care because cross-contact and preparation conditions matter.
From a data-modeling perspective, though, keeping allergen information structured gives you far more flexibility than storing everything as prose.
## Add Source Metadata
This is one field I think many content-driven applications forget.
Restaurant information changes.
So I like storing where information came from and when it was last checked.
For example:
```javascript id="te5xn8"
{
id: 101,
name: "Chicken Biscuit",
source: {
type: "restaurant_menu",
lastVerified: "2026-08-01"
}
}
If you're aggregating or maintaining reference data, you may also keep the source URL internally.
For example, while looking at how a real-world menu information site organizes categories and pricing, a resource such as this bojangles menu provides a useful example of the kind of data a menu application may need to represent.
The key point is that source metadata should be separate from the actual product fields.
Create a Last-Updated Field
Restaurant data becomes stale.
Your schema should acknowledge that.
```javascript id="kv1n2z"
{
id: 101,
name: "Chicken Biscuit",
updatedAt: "2026-08-01T12:00:00Z"
}
Then you can identify records that need verification:
```javascript id="g4fm2x"
const oldItems = menuItems.filter(item => {
const updated = new Date(item.updatedAt);
const age = Date.now() - updated.getTime();
return age > 30 * 24 * 60 * 60 * 1000;
});
Now stale-data detection can become part of the application rather than a manual process.
A More Complete Menu Object
After accounting for these requirements, a menu item could look something like:
```json id="qp3x91"
{
"id": 101,
"slug": "chicken-biscuit",
"name": "Chicken Biscuit",
"category_id": "breakfast",
"description": "Chicken served on a biscuit",
"nutrition": {
"calories": 620,
"protein_g": 24
},
"allergens": [
"wheat",
"milk"
],
"image": {
"url": "/images/chicken-biscuit.webp",
"alt": "Chicken biscuit"
},
"status": "active",
"updated_at": "2026-08-01T12:00:00Z"
}
Pricing and availability can remain in separate collections.
That gives us:
```text id="v9z8dm"
Products
|
+--- Categories
|
+--- Nutrition
|
+--- Allergens
Locations
|
+--- Prices
|
+--- Availability
|
+--- Hours
This structure is much easier to scale than one giant object containing everything.
Building the Frontend
Once the data is structured properly, rendering becomes straightforward.
``javascript id="m1wz8a"
function MenuCard({ item }) {
return
src="${item.image.url}"
alt="${item.image.alt}"
/>
<h2>${item.name}</h2>
<p>${item.description}</p>
</article>
`;
}
Filtering by category is also simple:
```javascript id="q8km2s"
function getItemsByCategory(categoryId) {
return menuItems.filter(
item => item.categoryId === categoryId
);
}
The frontend doesn't need to understand how the underlying information was collected.
It just consumes clean data.
Search Becomes Easier Too
A basic search implementation could be:
```javascript id="bn2v9p"
function searchMenu(query) {
const normalized = query.toLowerCase();
return menuItems.filter(item =>
item.name.toLowerCase().includes(normalized) ||
item.description.toLowerCase().includes(normalized)
);
}
Later, you can replace this with:
* database full-text search
* Algolia
* Elasticsearch
* Meilisearch
without redesigning the actual menu schema.
## Don't Put Everything in One Table
For a small demo project, one table is fine.
For a larger application, I would probably separate:
```text id="e5jx7n"
categories
products
locations
prices
availability
nutrition
allergens
product_allergens
For example:
```sql id="w2pr8k"
CREATE TABLE products (
id BIGINT PRIMARY KEY,
category_id BIGINT,
name VARCHAR(255),
slug VARCHAR(255),
description TEXT,
status VARCHAR(50),
updated_at TIMESTAMP
);
And:
```sql id="ak7d1c"
CREATE TABLE prices (
id BIGINT PRIMARY KEY,
product_id BIGINT,
location_id BIGINT,
price DECIMAL(10,2),
currency CHAR(3),
updated_at TIMESTAMP
);
Now product information and pricing can evolve independently.
Cache the Read-Heavy Parts
Menu applications are generally read-heavy.
Thousands of visitors may read the same information while relatively few updates occur.
That's a perfect candidate for caching.
For example:
```text id="h3tf9q"
Request
|
v
Cache
|
+--- HIT -> Return menu
|
+--- MISS
|
v
Database
|
v
Cache result
You could cache:
* categories
* menu pages
* individual products
* location menus
and invalidate the relevant cache when information changes.
## The Bigger Lesson
The interesting part of a restaurant menu application isn't the HTML card.
It's modeling data that changes over time.
The same principles apply to many other projects:
```text id="j8p3w2"
Restaurant menus
Product catalogs
Hotel listings
Event directories
Price comparison sites
Travel databases
Separate the stable entity from the information that varies.
For restaurant data:
```text id="r5vz2n"
Product = relatively stable
Price = variable
Availability = variable
Location = variable
Hours = variable
Once you model those concepts independently, the rest of the application becomes much easier to maintain.
## Final Thoughts
A restaurant menu can be a surprisingly good project for learning practical data modeling.
Start with products and categories.
Then introduce location-specific pricing, availability, nutrition, allergens, schedules, source metadata, and caching only when your requirements actually need them.
Most importantly, don't design your database around the page you're looking at today.
Design it around the information your application needs to represent.
That gives you a system that can grow without requiring a complete rewrite every time the menu changes.
Top comments (0)