A small polls API built with Ruby on Rails (API-only). Create a poll with options, let people vote once each, and read back live results with vote counts and percentages.
# create a poll
curl -X POST localhost:3000/polls \
-H 'Content-Type: application/json' \
-d '{"question":"Tabs or spaces?","options":["Tabs","Spaces"]}'
# vote (one vote per voter token per poll)
curl -X POST localhost:3000/polls/1/votes \
-H 'Content-Type: application/json' \
-d '{"option_id":1,"voter_token":"anon-abc"}'
# {"id":1,"question":"Tabs or spaces?","closed":false,"total_votes":1,
# "options":[{"id":1,"label":"Tabs","votes":1,"percentage":100.0},
# {"id":2,"label":"Spaces","votes":0,"percentage":0.0}]}A polls API is a small domain with a surprisingly complete validation story, which makes it a good, honest demonstration of a Rails backend: associations between polls, options and votes; validations that enforce real rules ("one vote per voter", "an option must belong to the poll", "no voting on a closed poll"); a uniqueness constraint backed at both the model and database level; and a clean JSON error contract. It is deliberately compact so the Rails idioms are the subject.
GET /polls list polls, newest first
POST /polls create a poll { question, options: [..] } (needs >= 2 options)
GET /polls/:id a poll with results (counts and percentages)
POST /polls/:id/close close a poll to further voting
POST /polls/:id/votes cast a vote { option_id, voter_token }
GET /up health check
Responses are JSON. Validation failures return 422 with an errors array; unknown records return
404. For example, a second vote from the same voter_token returns:
{ "errors": ["Voter token has already voted in this poll"] }Poll (question, closed)
has_many :options
has_many :votes
Option (poll_id, label)
belongs_to :poll
has_many :votes
Vote (poll_id, option_id, voter_token)
belongs_to :poll
belongs_to :option
# unique on (poll_id, voter_token), in the model and as a DB index
Keeping poll_id on the vote (alongside option_id) is what lets "one vote per voter per poll" be a
simple scoped uniqueness rule, enforced both by an ActiveRecord validation and a unique database
index so a race cannot slip a duplicate through.
- A poll needs a question and at least two options.
- A voter (identified by an opaque
voter_token) may vote once per poll, but the same token can vote in other polls. - A vote's option must belong to the poll it is cast in.
- No votes once a poll is closed.
Each of these is covered by a test.
bundle install
bin/rails db:prepare
bin/rails server # http://localhost:3000bin/rails db:prepare
bin/rails testTwenty-two tests with Rails' built-in Minitest: model tests for the vote rules and the results calculation, and request tests that drive the JSON API end to end, asserting status codes, the error contract and the computed percentages. CI runs the suite against a fresh SQLite database on every push.
app/models Poll, Option, Vote (associations and validations)
app/controllers PollsController, VotesController, ApplicationController (JSON error handling)
config/routes.rb nested RESTful routes
db/migrate the three tables and the unique vote index
test/ model and request tests
This repository was written as an API-only Rails app; db/schema.rb is generated by db:migrate
rather than committed, so CI migrates a fresh database for each run.
- Poll expiry (auto-close after a deadline).
- Rate limiting and abuse protection on voting.
- A results endpoint that streams live updates over Action Cable.
MIT