A simple single-page poll application. Users create a poll, get a short link, share it, and vote. Results appear immediately after voting.
- Backend: Laravel 11 (PHP), SQLite, DDD + Clean Architecture
- Frontend: Vue 3, Vuex, Vue Router, Axios, Vite
/
├── backend/ # Laravel API
└── frontend/ # Vue 3 SPA
- PHP >= 8.2
- Composer
- Node.js >= 18
- npm
cd backend
# 1. Install PHP dependencies
composer install
# 2. Copy environment file (already configured for SQLite)
cp .env.example .env
# 3. Generate app key
php artisan key:generate
# 4. Run migrations (creates polls, options, votes tables)
php artisan migrate
# 5. Start the development server
php artisan serveThe API will be available at http://localhost:8000.
cd frontend
# 1. Install dependencies
npm install
# 2. Start the dev server
npm run devThe app will be available at http://localhost:5173.
| Method | URL | Description |
|---|---|---|
POST |
/api/polls |
Create a poll |
GET |
/api/polls/{short_code} |
Get poll data (+ voted status by IP) |
POST |
/api/polls/{short_code}/vote |
Cast a vote |
{
"title": "Favorite color?",
"options": ["Red", "Blue", "Green"]
}Response 201:
{ "short_code": "aBc123" }{ "option_id": 2 }Response 200:
{
"votes": [
{ "option_id": 1, "option": "Red", "count": 3 },
{ "option_id": 2, "option": "Blue", "count": 5 }
]
}Returns 409 Conflict if the IP has already voted.
app/
├── Domain/Poll/
│ ├── Contracts/CodeGeneratorInterface.php # Strategy interface for code generation
│ ├── Services/RandomCodeGenerator.php # Default implementation
│ └── Factories/PollFactory.php # Domain factory (injectable strategy)
├── Application/Poll/UseCases/
│ ├── CreatePollUseCase.php
│ ├── GetPollUseCase.php
│ └── VoteUseCase.php
├── Http/Controllers/Api/PollController.php
├── Http/Requests/
│ ├── CreatePollRequest.php
│ └── VoteRequest.php
└── Providers/PollServiceProvider.php # Binds interface to implementation
Swapping the code generation strategy — inject a different CodeGeneratorInterface implementation via PollServiceProvider:
// e.g. bind a custom NanoIdGenerator instead
$this->app->bind(CodeGeneratorInterface::class, NanoIdGenerator::class);src/
├── api/index.js # Axios instance
├── store/index.js # Vuex store (poll, results, hasVoted)
├── router/index.js # /create and /poll/:code routes
├── views/
│ ├── CreatePoll.vue # Poll creation form
│ └── PollPage.vue # Voting / results page
└── components/
├── VoteForm.vue # Radio options + submit
└── PollResults.vue # Results with bar chart
Votes are deduplicated by (poll_id, ip_address) unique constraint in the database. A repeat vote returns 409 Conflict. On page load, the backend checks the IP and returns results immediately if already voted.