In this tutorial we'll be building a polling system from scratch using Encore.ts.
Picture it: You and your friends are trying to decide on a weekend getaway and everyone has different opinions. Some are for one place and others for another.
So you came up with the idea to create a poll where everyone can vote sounds fun right?
There’s something special about creating something from nothing and sharing it with your friends.
That’s exactly what we’re going to do! We’re going to build a full-blown polling system like the ones you see on Messenger, WhatsApp, or YouTube community posts.
In this tutorial, we’ll build an end-to-end Online Polling system. We’ll focus on building the backend using REST APIs with Encore.ts and integrating the API with the front end.
I’ll walk you through step by step from scratch to deploying the backend service using the Encore Cloud platform. We’ll use PostgreSQL as the database to store our poll data.
What is Encore?
Encore is an open-source backend framework for building scalable, distributed systems. It’s a dev-friendly tool that makes building robust, type-safe applications easy with it’s high-performance API framework. Whether you like TypeScript or Go, Encore supports both.
Encore also comes with built-in tools to help with development whether you’re working on a small personal project or a large-scale backend system.
It’s a great choice for developers who value simplicity, performance, and scalability.
Prerequisites
No special expertise is needed to follow along with this tutorial. But since we’re building a REST API, it would be helpful if you have a basic understanding of how REST APIs work. Things like the common methods: GET, POST, PUT, DELETE will come in handy as we go.
If you’ve worked with Node.js or Express before, even just a little, that’s a bonus. Knowing how to create server routes or set up a basic server will help you connect the dots as you go.
Before we get started, make sure you have the latest version of Node.js installed.
Go to Node.js Website and download it if you haven’t already. Once that’s done we’re good to go.
Before we get started, one more thing: the Encore CLI. Go to encore.dev and you’ll find all the information you need to install it.
Follow the instructions for your machine if you’re on Windows, macOS, or Linux.
You can also copy and paste the following commands to install it directly:
# Install Encore on macOS
brew install encoredev/tap/encore
# Install Encore on Windows (PowerShell)
iwr https://encore.dev/install.ps1 | iex
# Install Encore on Linux
curl -L https://encore.dev/install.sh | bash
Now that you have everything installed, let’s check Node.js and Encore are set up on your machine. Like when you start a new project and want to make sure you have all your tools in place.
Here’s how you can check node version:
node -v
This should show you the version of Node.js you’ve installed. If it pops up with a version number, you’re good to go.
Let’s check the encore version:
encore version
If Encore is installed properly, it’ll display its version too.
Seeing both version numbers?
Perfect. You’re all set, and we’re ready to move forward.
If not, double-check the installation steps, and we’ll get it sorted.
Setting Up the Project
Now let’s create our Encore project. Just run the command and it’ll walk you through the process to set up the encore project.
Here is the command to create an encore project:
encore app create
When you run this, Encore will ask you to choose a language for your project. In this tutorial, we’ll go with TypeScript it’s a great choice for building robust and scalable APIs.
Use your arrow keys to select TypeScript, then hit Enter.
When you run the encore app create
command, Encore gives you a few templates to choose from.
These are like pre-built blueprints, each one to help you get started depending on what kind of project you’re building.
But for this tutorial, we’re going to keep it simple and start from scratch. We’ll choose the “Empty app” template.
Why?
Because building from scratch is the best way to see how everything works.
So, go ahead and select the Empty app template. Encore will give you a clean, empty project for you .
Once you do, Encore will generate a clean, minimal project structure for you. No extra code, no pre-built features just a blank canvas waiting for your ideas.
Now comes the fun part of naming your project. For this tutorial, I’ve named my project “polling-system”, but feel free to choose something that resonates with you.
As soon as you hit Enter, Encore will spring into action. It’ll start downloading the “Empty app” template and setting up your project behind the scenes.
You’ll see a flurry of activity in your terminal files being created, dependencies being installed, and everything neatly organized into a clean project structure.
Once the setup is complete, Encore will likely prompt you to run the project right away. It’ll suggest a command like encore run
to fire things up.
But hold on we’re not going to run it just yet. Instead, we’ll switch over to our code editor, where we’ll open the project, explore the structure, and run it from there.
It’s a better way to stay in sync with the code as we build and tweak things.
When you open the project in your code editor, it’s refreshingly simple. No clutter, no overwhelming list of dependencies.
If you peek into the package.json
, you’ll see just one main dependency: encore.dev. That’s it.
Alongside it, TypeScript sits quietly as a dev dependency, keeping things clean and focused.
Open your terminal in your code editor and run the following command:
encore run
As soon as you hit Enter, something magical happens. Encore doesn’t just start your project it automatically opens your browser and takes you straight to the developer dashboard.
As we start building our APIs, we’ll explore it in detail tracking requests, debugging errors, and much more.
But for now, just take a moment to appreciate how seamless this is. Encore is doing the heavy lifting so you can focus on what really matters: writing great code.
Let’s keep going there’s so much more to uncover!
Implementing the API Endpoints
Alright, let’s create our first API endpoint. We’re going to build a simple /hello
endpoint that responds with “Hello, world!” when you hit it.
Let's explore how we can accomplish this. Now, let's return to the code editor.
I’ve created a directory called hello right at the root of the project, and inside it, I added a file named hello.ts
.
This is how we can keep our project organized structuring folders and files in a way that makes sense as the project grows.
Inside the hello.ts
file, I imported the api
function from Encore. This function is the heart of defining endpoints.
It takes two main things:
An object as the first parameter, where we define the method (like GET, POST, etc) the path (like
/hello
), and a key calledexpose: true
. Settingexpose: true
means this endpoint is publicly accessible anyone can call it.An asynchronous function, where we write the logic for what the endpoint should return. In our case, it’s a simple “Hello, world!” message.
I hope this gives you a basic idea of how endpoints are structured and what to keep in mind when working with them.
Now that we’ve got the fundamentals down, let’s shift gears and start building the REST APIs for our online polling system.
This is where things get exciting!
Create Poll:
Let’s start by creating a new directory called polls at the root of our project. Inside this directory, we’ll add a file named polls.ts
.
This is where we’ll define everything related to our polls functionality keeping things neat and organized as we build.
Next, let’s define the interfaces for our polls.
// Data Models
interface Poll {
id: number;
question: string;
created_at: string;
}
interface Option {
id: number;
poll_id: number;
option_text: string;
vote_count: number;
}
interface CreatePollParams {
question: string;
options: string[];
}
interface VoteParams {
pollId: number;
optionId: number;
}
Here made things simple easy to understand and efficient. While createing poll in polls table we will only keep question
nothing more than that.
Then, in the options table we keep poll_id
along with option *text
and vote_*count
.
Setup your local database
The first thing you need is Docker running on your machine. Encore uses this to automatically setup and manage your local databases.
Don’t worry, if you do not have Docker, just go to the website and download the version that is suitable for your operating system. The installation is simple so you will be able to create a PostgreSQL instance very quickly.
Once Docker is started, you don’t need to do anything else with it for now.
Now, let’s focus on creating a database schema for our polls API. To do this, we’ll use migration files.
Migrations are like a version control for your db they help you define and update your db over time.
Here’s how we’ll set it up:
Inside the polls directory (which we created earlier), we’ll add another directory called
migrations
. This is where all our database migration files will live.Inside the migrations folder, we’ll create a file named
1_create_tables.up.sql
The name might look a bit specific, but it’s just a convention the1
indicates it’s the first migration, and.up.sq
means this file will handle creating or updating the database schema.
In this file we’ll define the schema for our polls
and options
table.
Here’s an example:
CREATE TABLE polls (
id SERIAL PRIMARY KEY,
question TEXT NOT NULL,
created_at TIMESTAMP DEFAULT now()
);
CREATE TABLE options (
id SERIAL PRIMARY KEY,
poll_id INTEGER REFERENCES polls(id) ON DELETE CASCADE,
option_text TEXT NOT NULL,
vote_count INTEGER DEFAULT 0
);
This polls schema includes:
-
id
column that auto-increments with each new poll. -
question
columns to store the only question text of the poll. -
created_at
column to track when the poll was created.
and in options table:
-
id
column that auto-increments with each new poll. -
poll_id
column to store the poll ID as a reference. -
options_text
column to store option text. Each poll should have exactly 4 options. -
vote_count
field that updates when a user votes for an option
Once this migration file is in place, Encore will automatically apply it to the database when we run our app using encore run
.
And just like that, our database will be ready to store polls!
Now, to we're going to use our database from our application. To do this, we’ll need to import the SQLDatabase from Encore’s sqldb
module. This will allow us to interact with our PostgreSQL database seamlessly.
import { SQLDatabase } from "encore.dev/storage/sqldb";
Once we’ve imported it we’ll define a database instance. Since our database is named polls
, we’ll use that as the reference.
Inside the database instance, we’ll also need to define the path to our migrations
folder. This tells Encore where to find the migration files we created earlier.
const db = new SQLDatabase("polls", { migrations: "./migrations" });
With this setup, Encore knows exactly where to find the migration files and will automatically apply them to the polls
database when we run the app.
It’s a clean and efficient way to manage our database schema as we build out the polling system.
Let's finally connect all the things together to test our create poll API.
import { api, APIError } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("polls", { migrations: "./migrations" });
interface Poll {
id: number;
question: string;
created_at: string;
}
interface Option {
id: number;
poll_id: number;
option_text: string;
vote_count: number;
}
interface CreatePollParams {
question: string;
options: string[];
}
// Create a Poll
export const createPoll = api<CreatePollParams, Poll>(
{
method: "POST",
path: "/polls",
expose: true,
},
async ({ question, options }: CreatePollParams): Promise<Poll> => {
if (options.length < 2 || options.length > 4) {
throw APIError.invalidArgument("Poll must have between 2 and 4 options.");
}
const poll = await db.queryRow<Poll>`
INSERT INTO polls (question)
VALUES (${question})
RETURNING id, question, created_at`;
if (!poll) {
throw APIError.internal("Failed to create poll");
}
for (const optionText of options) {
await db.exec`
INSERT INTO options (poll_id, option_text, vote_count)
VALUES (${poll.id}, ${optionText}, 0)`;
}
return poll;
}
);
Here we have the condition to check options are meet our expectation that has to be greater 2 or less then equal 4.
Then we have added questions to the polls table and options in the options table.
Once you re-run your app, you’ll notice that after the process completes in Docker, a container is created for your database.
Cool, let’s test our endpoint on the Developer Dashboard. Head over to the dashboard, and you’ll see your endpoint listed there.
Now, when we check the response, we can see the id
, question
, andcreated_at
fields. This means our data has been successfully saved in the database.
Since we are saving only questions in polls table and options in the options table, so It’s working perfectly!
If you want to take a closer look at the tables and data, you can use the Encore DB Shell. It’s a handy tool that lets you interact directly with your database.
To open the shell, just run this command in your terminal:
encore db shell polls
Here, polls
is the name of our database. Once you run this, you’ll be inside the database shell, where you can explore tables, run SQL queries, and see exactly what’s stored in your database.
Let’s give it a try and see what’s under the hood!
Now we can query from our table.
I have created some polls and we can see all the options here with vote_count. The data is being saved correctly, and we’ve verified it.
Read All Polls:
Now, let’s create our second endpoint this time to retrieve all the polls from our database.
Just like we did for the createPoll
endpoint, we’ll follow a similar process. We’ll define the endpoint, write the logic to fetch the data from the database and return it as a response.
It’s all about building on what we’ve already learned. Let’s dive in and get this done!
// Get All Polls
export const getPolls = api(
{
method: "GET",
path: "/polls",
expose: true,
},
async (): Promise<{ polls: Poll[] }> => {
const rows = db.query`
SELECT id, question, created_at
FROM polls
ORDER BY created_at DESC`;
const polls: Poll[] = [];
for await (const row of rows) {
polls.push({
id: row.id,
question: row.question,
created_at: row.created_at,
});
}
return { polls };
}
);
Let’s test our new endpoint and see if all the polls are showing up correctly.
Head over to the Developer Dashboard, find the getPolls
endpoint in the dropdown, and hit send. If everything’s set up right, you should see a list of all the polls stored in your database.
Get poll by ID:
Now, let’s create an endpoint to get a single poll by its ID. For this, we’ll take the id
as a parameter in the function.
Then, we’ll write a simple SQL query to fetch the poll from the database that matches the given id
. It’s straightforward just a small tweak to what we’ve already done.
Let’s build it and see how it works!
// Get a Poll by ID
export const getPollById = api<
{ id: number },
{ poll: Poll; options: Option[] }
>(
{
method: "GET",
path: "/polls/:id",
expose: true,
},
async ({
id,
}: {
id: number;
}): Promise<{ poll: Poll; options: Option[] }> => {
const poll = await db.queryRow<Poll>`
SELECT id, question, created_at FROM polls WHERE id = ${id}`;
if (!poll) {
throw APIError.notFound("Poll not found");
}
const optionsRows = db.query`
SELECT id, option_text, vote_count FROM options WHERE poll_id = ${id}`;
const options: Option[] = [];
for await (const row of optionsRows) {
options.push({
id: row.id,
poll_id: id,
option_text: row.option_text,
vote_count: row.vote_count,
});
}
return { poll, options };
}
);
Let’s test this new endpoint using the Developer Dashboard. Head over to the dashboard, find the getPollById
endpoint, and pass 1
as the ID. Hit send, and you should see the details of that single poll in the response.
Vote Poll by ID:
Now, let’s create an endpoint to vote poll by its ID. For this, we’ll take the id
as a parameter in the function and optionId
from the body.
Then, we’ll write a simple SQL query to vote the poll. We will update the vote_count value based on the optionId
. Let’s build it and see how it works!
// Vote for an Option
export const votePoll = api<VoteParams, { message: string }>(
{
method: "POST",
path: "/polls/:pollId/vote",
expose: true,
},
async ({ pollId, optionId }: VoteParams): Promise<{ message: string }> => {
const option = await db.queryRow<Option>`
SELECT * FROM options WHERE id = ${optionId} AND poll_id = ${pollId}`;
if (!option) {
throw APIError.notFound("Invalid option.");
}
await db.exec`
UPDATE options SET vote_count = vote_count + 1 WHERE id = ${optionId}`;
return { message: "Vote recorded successfully" };
}
);
Let's test our Vote Poll endpoint:
Get Poll Result:
Now, let’s create an endpoint to get result by poll ID. For this, we’ll take the id
as a parameter in the function.
Then, we’ll write a simple SQL query to get the vote result by the poll ID. Let’s build it and see how it works!
// Get Poll Results
export const getPollResults = api<{ id: number }, { results: Option[] }>(
{
method: "GET",
path: "/polls/:id/results",
expose: true,
},
async ({ id }: { id: number }): Promise<{ results: Option[] }> => {
const optionsRows = db.query`
SELECT id, option_text, vote_count FROM options WHERE poll_id = ${id}
ORDER BY vote_count DESC`;
const options: Option[] = [];
for await (const row of optionsRows) {
options.push({
id: row.id,
poll_id: id,
option_text: row.option_text,
vote_count: row.vote_count,
});
}
if (options.length === 0) {
throw APIError.notFound("Poll not found.");
}
return { results: options };
}
);
Let's test the endpoint on the developer dashboard:
Now that we’ve walked through all the operations for our online polling system API.
Create Poll, Get poll by ID, Vote poll, and Get Poll result. You’ve got the foundation to build a fully functional application.
I hope this gives you a solid understanding of how to create and manage an application using Encore.
Deploy the backend server:
Deployment in Encore is one of those things that feels almost magical it’s incredibly interesting and super easy.
With just 3 commands, you can deploy your entire backend code, including the database.
Here’s how it works:
-
Stage your changes:
git add .
-
Commit your changes with a message:
git commit -m "Your commit message here"
-
Push to Encore:
git push encore
And that’s it! Encore takes care of the rest building your application, setting up the database, and deploying everything to the cloud.
Now open your app from app.encore.dev
Here’s your live hosted URL!
You can now use this URL to integrate with frontend that we will do in a bit.
And guess what? It’s already hosted in a staging environment.
How cool is that? In just a minute, your application is up and running, ready to be tested and used.
Encore makes deployment feel effortless.
API Integration with Frontend
We have all the endpoints, they’re in the Encore environment. Now it’s time to bring it all to life by adding a frontend.
For this, I’m creating a React app using shadcn UI—a sleek and modern UI library that’ll make our online polling platform look polished and professional.
Now that the backend is set up, let’s connect it to the frontend!
// App.tsx
import { useEffect, useState } from "react";
import CreatePollDialog from "./components/CreatePollDialog";
import PollCard from "./components/PollCard";
import { CreatePollData, Poll } from "./types";
const base_url = "http://127.0.0.1:4000";
function App() {
const [polls, setPolls] = useState<Poll[]>([]);
useEffect(() => {
fetchPolls();
}, []);
const fetchPolls = async () => {
try {
const response = await fetch(`${base_url}/polls`);
const data = await response.json();
console.log(data);
const pollsWithOptions = await Promise.all(
data.polls.map(async (poll: Poll) => {
const detailResponse = await fetch(`${base_url}/polls/${poll.id}`);
const detailData = await detailResponse.json();
return {
...poll,
options: detailData.options,
};
})
);
console.log(pollsWithOptions);
setPolls(pollsWithOptions);
} catch (error) {
console.error("Failed to fetch polls:", error);
}
};
const handleCreatePoll = async (data: CreatePollData) => {
try {
const response = await fetch(`${base_url}/polls`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (response.ok) {
fetchPolls();
}
} catch (error) {
console.error("Failed to create poll:", error);
}
};
const handleVote = async (pollId: number, optionId: number) => {
try {
const response = await fetch(`${base_url}/polls/${pollId}/vote`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ pollId, optionId }),
});
if (response.ok) {
const resultsResponse = await fetch(
`${base_url}/polls/${pollId}/results`
);
const { results } = await resultsResponse.json();
setPolls((prev) =>
prev.map((poll) =>
poll.id === pollId ? { ...poll, options: results } : poll
)
);
}
} catch (error) {
console.error("Failed to submit vote:", error);
}
};
return (
<div className="w-screen min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<div className="w-full px-4 py-8">
<div className="max-w-7xl mx-auto mb-8">
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h1 className="text-4xl font-bold text-gray-900 dark:text-white">
Community Polls
</h1>
<p className="mt-2 text-gray-600 dark:text-gray-300">
Create and vote on community polls
</p>
</div>
<CreatePollDialog onCreatePoll={handleCreatePoll} />
</div>
</div>
<div className="max-w-7xl mx-auto space-y-6">
{polls.map((poll) => (
<PollCard key={poll.id} poll={poll} onVote={handleVote} />
))}
{polls.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400">
No polls yet. Be the first to create one!
</p>
</div>
)}
</div>
</div>
</div>
);
}
export default App;
This CreatePollDialog
is a nice little dialog box for creating polls. We have 2 input boxes to define the poll question and add multiple options.
It’s simple and easy for users to interact and participate. Let’s see how it works and integrate it into our app!
// CreatePollDialog.tsx
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { CreatePollData } from "@/types";
import { Cross2Icon } from "@radix-ui/react-icons";
import { PlusCircle } from "lucide-react";
import { useState } from "react";
interface CreatePollDialogProps {
onCreatePoll: (data: CreatePollData) => Promise<void>;
}
export default function CreatePollDialog({
onCreatePoll,
}: CreatePollDialogProps) {
const [question, setQuestion] = useState("");
const [options, setOptions] = useState(["", ""]);
const [isOpen, setIsOpen] = useState(false);
const handleAddOption = () => {
if (options.length < 4) {
setOptions([...options, ""]);
}
};
const handleRemoveOption = (index: number) => {
if (options.length > 2) {
setOptions(options.filter((_, i) => i !== index));
}
};
const handleOptionChange = (index: number, value: string) => {
const newOptions = [...options];
newOptions[index] = value;
setOptions(newOptions);
};
const handleSubmit = async () => {
if (question.trim() && options.every((opt) => opt.trim())) {
await onCreatePoll({
question: question.trim(),
options: options.map((opt) => opt.trim()),
});
setQuestion("");
setOptions(["", ""]);
setIsOpen(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<PlusCircle className="h-5 w-5" />
Create Poll
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold"> Create a New Poll</h2>
<Cross2Icon
onClick={() => setIsOpen(false)}
className="h-4 w-4 cursor-pointer"
/>
</div>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="question">Question</Label>
<Input
id="question"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="What's your question?"
/>
</div>
<div className="space-y-4">
<Label>Options</Label>
{options.map((option, index) => (
<div key={index} className="flex items-center gap-2">
<Input
value={option}
onChange={(e) => handleOptionChange(index, e.target.value)}
placeholder={`Option ${index + 1}`}
/>
{options.length > 2 && (
<Cross2Icon
onClick={() => handleRemoveOption(index)}
className="h-4 w-4 cursor-pointer"
/>
)}
</div>
))}
{options.length < 4 && (
<Button
variant="outline"
className="w-full"
onClick={handleAddOption}
>
Add Option
</Button>
)}
</div>
</div>
<Button onClick={handleSubmit} className="w-full">
Create Poll
</Button>
</DialogContent>
</Dialog>
);
}
And the PollCard is where you vote on polls. It will also display a smooth animation, similar to YouTube community polls. Check out the demo video below.
// PollCard.tsx
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { Poll } from "@/types";
import { motion } from "framer-motion";
import { useState } from "react";
interface PollCardProps {
poll: Poll;
onVote: (pollId: number, optionId: number) => Promise<void>;
}
export default function PollCard({ poll, onVote }: PollCardProps) {
const [hasVoted, setHasVoted] = useState(false);
const [selectedOption, setSelectedOption] = useState<number | null>(null);
const totalVotes =
poll.options?.reduce((sum, opt) => sum + opt.vote_count, 0) || 0;
const handleVote = async (optionId: number) => {
if (!hasVoted) {
setSelectedOption(optionId);
await onVote(poll.id, optionId);
setHasVoted(true);
}
};
const getPercentage = (votes: number) => {
return totalVotes === 0 ? 0 : Math.round((votes / totalVotes) * 100);
};
return (
<Card className="w-full">
<CardHeader>
<h3 className="text-xl font-semibold">{poll.question}</h3>
<p className="text-sm text-muted-foreground">
Created {new Date(poll.created_at).toLocaleDateString()}
</p>
</CardHeader>
<CardContent className="space-y-4">
{poll.options?.map((option) => (
<div key={option.id} className="relative">
<Button
variant={
hasVoted && selectedOption !== option.id
? "secondary"
: "outline"
}
className={cn(
"w-full justify-start h-auto py-3 px-4 relative overflow-hidden",
selectedOption === option.id && "border-primary",
hasVoted &&
selectedOption !== option.id &&
"opacity-70 text-black"
)}
onClick={() => handleVote(option.id)}
disabled={hasVoted && selectedOption !== option.id}
>
{hasVoted && selectedOption === option.id && (
<motion.div
className="absolute left-0 top-0 bottom-0 bg-primary/20"
initial={{ width: 0 }}
animate={{ width: `${getPercentage(option.vote_count)}%` }}
transition={{ duration: 0.5, ease: "easeOut" }}
/>
)}
<div className="relative z-10 flex justify-between w-full">
<span>{option.option_text}</span>
{hasVoted && (
<span className="ml-2 font-medium">
{getPercentage(option.vote_count)}%
</span>
)}
</div>
</Button>
</div>
))}
</CardContent>
<CardFooter>
<p className="text-sm text-muted-foreground">
{totalVotes} {totalVotes === 1 ? "vote" : "votes"}
</p>
</CardFooter>
</Card>
);
}
Here’s the final product. Everything is live when you vote, it saves to the backend database and you get a smooth animation like YouTube community polls and other online polls.
Wrapping up
Wow, what a ride from setting up the project and creating the first API to connecting to a PostgreSQL DB and deploying to Encore. Seeing it all come together has been awesome.
Encore makes it so easy to build robust, type-safe apps that scale.
If you want a closer look, don't fail to check out the Documentation.
If you like the project please Star Encore on GitHub.
Thanks for reading till the end!
Top comments (4)
Great Article!
Thanks!
Helpful Guide!
Thanks!