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.
- Ruby 3.3.4
- PostgreSQL
| Concern | Library / Tool |
|---|---|
| Framework | Rails 8.1.2 (API-only) |
| Language | Ruby 3.3.4 |
| Database | PostgreSQL |
| Testing | RSpec, FactoryBot, Faker |
| Deployment | Docker, Kamal |
- Model specs — for validations, associations and future domain specifics.
- Request specs — verifies response structure, attributes, empty states, and basic error handling.
bin/setup # installs gems, creates & migrates the databasebin/rails server # http://localhost:3000bundle exec rspec- A
Menumodel with anameattribute (validated for presence, non-null at the DB level). - A
MenuItemmodel withnameandprice, belonging to aMenu(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.
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).
Returns a simple list of menus.
[
{ "id": 1, "name": "lunch" },
{ "id": 2, "name": "dinner" }
]Returns a single menu. Responds with 404 if the menu does not exist.
{ "id": 1, "name": "lunch" }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" }
]- 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.
- A
Restaurantmodel with anameattribute (validated for presence, non-null at the DB level). MenuandMenuItemboth belongs toRestaurant.- A
MenuEntryjoin model enabling a many-to-many relationship betweenMenuandMenuItem. AMenuItemcan appear on multipleMenuwithin the sameRestaurant. MenuItemnames are unique per restaurant (validated at model and DB level via a unique index on[restaurant_id, name]).MenuEntryenforces uniqueness of aMenuItemon a givenMenu(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.
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:
Restauranthas manyMenus andMenuItems.Menubelongs toRestaurant, has manyMenuItems throughMenuEntry.MenuItembelongs toRestaurant, has manyMenus throughMenuEntry.MenuEntrybelongs to bothMenuandMenuItem.
Returns a list of restaurants.
[
{ "id": 1, "name": "Disfrutar" },
{ "id": 2, "name": "Asador Etxebarri" }
]Returns a single restaurant. Responds with 404 if it does not exist.
{ "id": 1, "name": "Disfrutar" } parameters:
optional:
- restaurant_idReturns a list of menus.
[
{ "id": 1, "name": "lunch", "restaurant_id": 1 },
{ "id": 2, "name": "dinner", "restaurant_id": 1 }
]Returns a single menu. Responds with 404 if it does not exist.
{ "id": 1, "name": "lunch", "restaurant_id": 1 }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 }
]- 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.
MenuEntryresponsability expanded to includeprice, allowing the sameMenuItemto have different prices on differentMenus.- A
MenuImportServicethat 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.
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.
- 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 onMenuItem. 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