DEV Community

Cover image for How I’d Structure a Restaurant Menu Dataset for a Web App
Faheem Zia
Faheem Zia

Posted on

How I’d Structure a Restaurant Menu Dataset for a Web App

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
};
Enter fullscreen mode Exit fullscreen mode

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
  }
];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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"
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(", ");
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)