Skip to content

douglasep/interview_restaurant_menus

Repository files navigation

Restaurant Menus API

Hi! Thanks for taking the time to review my submission. This is a PUBLIC JSON API for reading restaurant menus, built with Rails 8.1 (API-only) and PostgreSQL.

I focused on keeping the code clean, tested, and easy to extend for future levels. Each level is built incrementally. Commits reflect that progression.


How to Run

Prerequisites

  • Ruby 3.3.4
  • PostgreSQL

Tech Stack

Concern Library / Tool
Framework Rails 8.1.2 (API-only)
Language Ruby 3.3.4
Database PostgreSQL
Testing RSpec, FactoryBot, Faker
Deployment Docker, Kamal

Test Coverage

  • Model specs — for validations, associations and future domain specifics.
  • Request specs — verifies response structure, attributes, empty states, and basic error handling.

Setup

bin/setup          # installs gems, creates & migrates the database

Start the Server

bin/rails server   # http://localhost:3000

Run Tests

bundle exec rspec

Level 1: Menus with many MenuItems

What I Built

  • A Menu model with a name attribute (validated for presence, non-null at the DB level).
  • A MenuItem model with name and price, belonging to a Menu (foreign key indexed).
  • Three read-only endpoints: list menus, fetch a single menu, and list a menu's items.
  • Full model and request specs for both resources.

Data Model

menus

Column Type Constraints
id bigint Primary key
name string Not null
created_at datetime Not null
updated_at datetime Not null

menu_items

Column Type Constraints
id bigint Primary key
name string Not null
price decimal(10,2) Not null, >= 0
menu_id bigint Not null, FK → menus, indexed
created_at datetime Not null
updated_at datetime Not null

Associations: Menu has many MenuItems (dependent: destroy).

Endpoints

GET /menus

Returns a simple list of menus.

[
  { "id": 1, "name": "lunch" },
  { "id": 2, "name": "dinner" }
]

GET /menus/:id

Returns a single menu. Responds with 404 if the menu does not exist.

{ "id": 1, "name": "lunch" }

GET /menus/:menu_id/menu_items

Returns all items for a given menu. Responds with 404 if the menu does not exist.

[
  { "id": 1, "name": "Burger", "price": "9.0" },
  { "id": 2, "name": "Small Salad", "price": "5.0" }
]

Key Decisions

  • Select-only queries - Controllers explicitly select required attributes to avoid over fetching and keep responses stable.
  • Scoped Menu Items - Menu items are accessed via /menus/:menu_id/menu_items, reflecting that they only exist in the context of a menu at this level.

Level 2: Multiple Menus

What I Built

  • A Restaurant model with a name attribute (validated for presence, non-null at the DB level).
  • Menu and MenuItem both belongs to Restaurant.
  • A MenuEntry join model enabling a many-to-many relationship between Menu and MenuItem. A MenuItem can appear on multiple Menu within the same Restaurant.
  • MenuItem names are unique per restaurant (validated at model and DB level via a unique index on [restaurant_id, name]).
  • MenuEntry enforces uniqueness of a MenuItem on a given Menu (unique index on [menu_id, menu_item_id]).
  • Read-only endpoints: list restaurants, show a restaurant, list menus, show a menu, and list a menu's items.
  • Full model specs (associations, validations) and request specs for all resources.

Data Model

restaurants

Column Type Constraints
id bigint Primary key
name string Not null
created_at datetime Not null
updated_at datetime Not null

menus

Column Type Constraints
id bigint Primary key
name string Not null
restaurant_id bigint Not null, FK → restaurants, indexed
created_at datetime Not null
updated_at datetime Not null

menu_items

Column Type Constraints
id bigint Primary key
name string Not null, unique per restaurant
price decimal(10,2) Not null, >= 0
restaurant_id bigint Not null, FK → restaurants, indexed
created_at datetime Not null
updated_at datetime Not null

menu_entries (join table)

Column Type Constraints
id bigint Primary key
menu_id bigint Not null, FK → menus, indexed
menu_item_id bigint Not null, FK → menu_items, indexed
created_at datetime Not null
updated_at datetime Not null

Unique index on [menu_id, menu_item_id] prevents duplicate items on the same menu.

Associations:

  • Restaurant has many Menus and MenuItems.
  • Menu belongs to Restaurant, has many MenuItems through MenuEntry.
  • MenuItem belongs to Restaurant, has many Menus through MenuEntry.
  • MenuEntry belongs to both Menu and MenuItem.

Endpoints

GET /restaurants

Returns a list of restaurants.

[
  { "id": 1, "name": "Disfrutar" },
  { "id": 2, "name": "Asador Etxebarri" }
]

GET /restaurants/:id

Returns a single restaurant. Responds with 404 if it does not exist.

{ "id": 1, "name": "Disfrutar" }

GET /menus

  parameters:
    optional: 
      - restaurant_id

Returns a list of menus.

[
  { "id": 1, "name": "lunch", "restaurant_id": 1 },
  { "id": 2, "name": "dinner", "restaurant_id": 1 }
]

GET /menus/:id

Returns a single menu. Responds with 404 if it does not exist.

{ "id": 1, "name": "lunch", "restaurant_id": 1 }

GET /menus/:menu_id/menu_items

Returns all items on a given menu (resolved through menu_entries). Responds with 404 if the menu does not exist.

[
  { "id": 1, "name": "Burger", "price": "9.0", "restaurant_id": 1 },
  { "id": 2, "name": "Small Salad", "price": "5.0", "restaurant_id": 1 }
]

Key Decisions

  • Restaurant as ownership boundary - Both menus and menu items belong to a restaurant, enabling reuse across menus while enforcing restaurant scoped uniqueness.
  • MenuEntry join model - A dedicated join model represents an item appearing on a menu, allowing many-to-many relationships without leaking implementation details into the API.
  • RESTful routing with limited nesting - Routes are kept readable by nesting only where the relationship is semantically meaningful (/menus/:menu_id/menu_items).
  • Defense-in-depth validation - Uniqueness constraints are enforced at both the model and database level.

Level 3: Menu Import Tool

What I Built

  • MenuEntry responsability expanded to include price, allowing the same MenuItem to have different prices on different Menus.
  • A MenuImportService that takes a specific JSON structure of restaurants, menus, and menu items, and persists them to the database. Can be used by API endpoint, rake task, or other further automation.
  • An API endpoint (POST /menus/import) to trigger the import process, accepting either a JSON file upload or a raw JSON body.

Data Model

restaurants

Column Type Constraints
id bigint Primary key
name string Not null
created_at datetime Not null
updated_at datetime Not null

menus

Column Type Constraints
id bigint Primary key
name string Not null
restaurant_id bigint Not null, FK → restaurants, indexed
created_at datetime Not null
updated_at datetime Not null

menu_items

Column Type Constraints
id bigint Primary key
name string Not null, unique per restaurant
restaurant_id bigint Not null, FK → restaurants, indexed
created_at datetime Not null
updated_at datetime Not null

menu_entries (join table)

Column Type Constraints
id bigint Primary key
menu_id bigint Not null, FK → menus, indexed
menu_item_id bigint Not null, FK → menu_items, indexed
price decimal(10,2) Not null, >= 0
created_at datetime Not null
updated_at datetime Not null

Unique index on [menu_id, menu_item_id] prevents duplicate items on the same menu.

Key Decisions

  • Domain namin (MenuItem vs Dish) - During this level, it became clear that MenuItem is somewhat overloaded as a domain concept. In practice, it represents a canonical dish offered by a restaurant (e.g. “Burger”), which can appear on multiple menus at different prices. A name like Dish or MenuDish might better reflect this responsibility. I chose not to rename the model to avoid unnecessary churn and to stay aligned with the assignment’s terminology, but in a real codebase I would validate the domain language with the team and consider renaming for clarity.
  • Pricing - Price is stored on MenuEntry, not on MenuItem. This allows the same dish to be priced differently across menus (e.g. lunch vs dinner)
  • Import service - The import logic is encapsulated in a dedicated service object (MenuImportService).
  • Import service performance - This is not seeking for performance, but rather separation of concerns and testability.
  • Import service strictness - It depends on a specific JSON structure, so it validates that structure and fails fast if it's not met

About

Rails API for restaurants and menus. Showcasing incremental design, data modeling, and a JSON import.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors