A Rails API application for managing racing data including races, teams, drivers, cars, and lap times.
┌──────────┐ ┌──────────┐ ┌─────────┐
│ Driver │ │ Team │ │ Race │
└────┬─────┘ └────┬─────┘ └────┬────┘
│ │ │
│ ┌─────────┘ │
└──────────| ┌────────┬─────────┘
│ │ │
│ │ │
┌────▼────┐ │ ┌────▼────┐
│ Car │──────┘ │ Lap │
└────┬────┘ └────┬────┘
│ │
└────────────────────┘
- Fields:
id,name(unique),created_at,updated_at - Relationships:
has_many :cars
- Fields:
id,first_name,last_name,birth_date,created_at,updated_at - Relationships:
has_many :cars
- Validations:
- First name and last name combination must be unique
- Birth date must be in the past
- Fields:
id,name,track_name,start_time,end_time,start_latitude,start_longitude,finish_latitude,finish_longitude,created_at,updated_at - Relationships:
has_many :carshas_many :laps
- Validations:
- Coordinates must be within valid ranges (-90 to 90 for latitude, -180 to 180 for longitude)
- Start time must be before end time
- Fields:
id,car_number,race_id,driver_id,team_id,created_at,updated_at - Relationships:
belongs_to :racebelongs_to :driverbelongs_to :teamhas_many :laps
- Validations:
- Car number must be unique within a race
- Car number must be >= 1
- Fields:
id,lap_number,race_id,car_id,start_time,end_time,lap_time_ms,created_at,updated_at - Relationships:
belongs_to :racebelongs_to :car
- Validations:
- Lap number must be unique per car per race
- Start time must be before end time
- Lap time must be > 0
-
Unique Constraints:
- Team names must be unique
- Driver first_name + last_name combination must be unique
- Car number must be unique within a race
- Lap number must be unique per car per race
- Race name + track_name + start_time combination must be unique
-
Foreign Keys:
- Cars reference: races, drivers, teams
- Laps reference: races, cars
All API endpoints are under the /api/v1 namespace and return JSON responses in JSON:API format.
http://localhost:3000/api/v1
| Method | Endpoint | Description |
|---|---|---|
| GET | /races |
List all races (paginated) |
| GET | /races/:id |
Get a specific race |
| POST | /races |
Create a new race |
| PATCH | /races/:id |
Update a race |
| DELETE | /races/:id |
Delete a race |
| GET | /races/:id/leaderboard |
Get race leaderboard |
| Method | Endpoint | Description |
|---|---|---|
| GET | /teams |
List all teams (paginated) |
| GET | /teams/:id |
Get a specific team |
| POST | /teams |
Create a new team |
| PATCH | /teams/:id |
Update a team |
| DELETE | /teams/:id |
Delete a team |
| GET | /stats/teams/:id |
Get team statistics |
| Method | Endpoint | Description |
|---|---|---|
| GET | /drivers |
List all drivers (paginated) |
| GET | /drivers/:id |
Get a specific driver |
| POST | /drivers |
Create a new driver |
| PATCH | /drivers/:id |
Update a driver |
| DELETE | /drivers/:id |
Delete a driver |
| GET | /stats/drivers/:id |
Get driver statistics |
| Method | Endpoint | Description |
|---|---|---|
| GET | /races/:race_id/cars |
List all cars in a race (paginated) |
| GET | /races/:race_id/cars/:car_number |
Get a specific car by car number |
| POST | /races/:race_id/cars |
Create a new car in a race |
| PATCH | /races/:race_id/cars/:car_number |
Update a car |
| DELETE | /races/:race_id/cars/:car_number |
Delete a car |
| Method | Endpoint | Description |
|---|---|---|
| GET | /races/:race_id/laps |
List all laps in a race (paginated) |
| GET | /races/:race_id/laps/:lap_number |
Get a specific lap by lap number |
| POST | /races/:race_id/cars/:car_number/laps |
Create a new lap for a car |
| PATCH | /races/:race_id/cars/:car_number/laps/:lap_number |
Update a lap |
| DELETE | /races/:race_id/cars/:car_number/laps/:lap_number |
Delete a lap |
| GET | /races/:race_id/cars/:car_number/laps |
Get all laps for a specific car |
- Pagination:
page- Page number (default: 1)per_page- Items per page (default: 25, max: 100)
curl -X POST http://localhost:3000/api/v1/races \
-H "Content-Type: application/json" \
-d '{
"race": {
"name": "F1 Grand Prix 2026",
"track_name": "Monaco",
"start_time": "2026-05-29 14:00:00",
"end_time": "2026-05-29 16:00:00",
"start_latitude": 43.7347,
"start_longitude": 7.4206,
"finish_latitude": 43.7347,
"finish_longitude": 7.4206
}
}'Response:
{
"data": {
"id": "1",
"type": "races",
"attributes": {
"name": "F1 Grand Prix 2026",
"track-name": "Monaco",
"start-time": "2026-05-29 14:00:00",
"end-time": "2026-05-29 16:00:00",
"start-latitude": 43.7347,
"start-longitude": 7.4206,
"finish-latitude": 43.7347,
"finish-longitude": 7.4206
},
"links": {
"self": "/api/v1/races/1"
}
}
}curl -X POST http://localhost:3000/api/v1/teams \
-H "Content-Type: application/json" \
-d '{
"team": {
"name": "Mercedes"
}
}'curl -X POST http://localhost:3000/api/v1/drivers \
-H "Content-Type: application/json" \
-d '{
"driver": {
"first_name": "Lewis",
"last_name": "Hamilton",
"birth_date": "1985-01-07"
}
}'curl -X POST http://localhost:3000/api/v1/races/1/cars \
-H "Content-Type: application/json" \
-d '{
"car": {
"car_number": 44,
"driver_id": 1,
"team_id": 1
}
}'curl -X POST http://localhost:3000/api/v1/races/1/cars/44/laps \
-H "Content-Type: application/json" \
-d '{
"lap": {
"lap_number": 1,
"start_time": "2026-05-29 14:05:00",
"end_time": "2026-05-29 14:06:15",
"lap_time_ms": 75915
}
}'# Get first page with 10 items per page
curl "http://localhost:3000/api/v1/races?page=1&per_page=10"Response:
{
"data": [
{
"id": "1",
"type": "races",
"attributes": { ... }
}
],
"meta": {
"current_page": 1,
"per_page": 10,
"total_pages": 3,
"total_count": 25
}
}curl http://localhost:3000/api/v1/races/1/leaderboardcurl http://localhost:3000/api/v1/stats/teams/1curl http://localhost:3000/api/v1/stats/drivers/1curl http://localhost:3000/api/v1/races/1/cars/44/lapsAll errors follow a consistent format:
{
"error": "Validation failed",
"details": [
"Name can't be blank",
"Start time must be before end time"
]
}Common HTTP Status Codes:
200 OK- Successful GET, PATCH201 Created- Successful POST204 No Content- Successful DELETE400 Bad Request- Invalid request parameters404 Not Found- Resource not found422 Unprocessable Entity- Validation errors500 Internal Server Error- Server error
- Ruby 2.7+ (or as specified in
.ruby-version) - Bundler
- SQLite3
-
Copy the
race_api_assingmentfolder in your computer -
Install dependencies:
bundle install- Set up the database:
rails db:create
rails db:migrate
rails db:seed- Start the server:
rails serverThe API will be available at http://localhost:3000
# Run all tests
bundle exec rspec
# Run specific test files
bundle exec rspec spec/models
bundle exec rspec spec/requests- Model Specs (
spec/models/): Test validations, associations, and model methods - Request Specs (
spec/requests/): Test API endpoints and responses - Factories (
spec/factories/): Test data factories using FactoryBot
The test suite includes:
- Model validations and associations
- API endpoint functionality
- Error handling
- Pagination
- Nested resource operations
- Framework: Ruby on Rails 8.1.2
- Database: SQLite3
- Serialization: ActiveModel::Serializers (JSON:API adapter)
- Sergio's trade off:
- Pros: Allows me to return JSON:Api responses pretty simple, and it's well known between devs, so it's easy to understand.
- Cons: I know there are best options out there like Fast JSON API, but I didn't have the time to try it before this assignment.
- Sergio's trade off:
- Pagination: Kaminari
- Sergio's trade off:
- Pro: I like because is more Rails way, cleaner and okay for a smaller API.
- Con: It's a bit slower and could use more memory than other gems.
- Sergio's trade off:
- Testing: RSpec, FactoryBot, Shoulda Matchers
- Sergio's trade off:
- Pro: It's an industry standard
- Con: I prioritized feature completion over full test-suite optimization since I was running out of time, sorry!
- Sergio's trade off:
- All timestamps are in UTC
- Lap times are stored in milliseconds (bigint)
- Car numbers and lap numbers are used as route parameters instead of IDs for nested routes
- The API uses JSON:API format, which converts snake_case attributes to kebab-case in responses
- Relations (associations) are only included in single resource responses, not in collection responses