In this tutorial, we’ll build a practical and fun project: a Personal Blogging Platform. We’ll focus on building the backend using REST APIs with Encore.ts.
I’ll walk you through step by step from scratch to deploying the backend service on using Encore Cloud's free hosting platform. We’ll use PostgreSQL as the database to store our blog data.
Exciting? Let’s get started!
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
You don’t need to be an Encore expert 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 naming your project. For this tutorial, I’ve named my project “blogs”, 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 your 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.
Now let’s open our project in your code editor. There’s another feature I want to show you that’s going to make your life as a dev so much easier.
Trust me you’re going to love this. Let’s get in!
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 blogging platform. This is where things get exciting!
Create blog :
Let’s start by creating a new directory called blog at the root of our project. Inside this directory, we’ll add a file named blog.ts. This is where we’ll define everything related to our blog functionality keeping things neat and organized as we build.
Next, let’s define the interfaces for our blog. Since we’re building a simple blogging platform, we’ll keep things straightforward. For now, we’ll need just three fields to create a blog:
- title: The headline of the blog.
- content: The main body of the blog post.
- author: The name of the person writing the blog.
Here’s how we’ll structure it in TypeScript:
interface Blog {
id: number;
title: string;
content: string;
author: string;
created_at: string;
updated_at: string;
}
interface CreateBlogParams {
title: string;
content: string;
author: string;
}
So here, now we have defined the createBlog
function that is basically a post endpoint. It will take blog information to create a blog.
import { api } from "encore.dev/api";
interface Blog {
id: number;
title: string;
content: string;
author: string;
created_at: string;
updated_at: string;
}
interface CreateBlogParams {
title: string;
content: string;
author: string;
}
// Create Blog
export const createBlog = api<CreateBlogParams, Blog>(
{
method: "POST",
path: "/blogs",
expose: true,
},
async ({ title, content, author }: CreateBlogParams) => {
return { title, content, author } as Blog;
}
);
Let’s use the Developer Dashboard that Encore provides. Think of it as Postman on steroids but with way more features. When you open the dashboard you’ll see a dropdown of all the endpoints you’ve created.
For example, if you’ve created a createBlog
endpoint it’ll be right there in the list. Every endpoint you build will show up here.
We’ve already tested our createBlog
endpoint by sending some sample data like a blog title, content and author name, its works perfectly.
The control panel not only shows the requests you send, but also the responses you receive - very intuitive and makes debugging and testing easier.
Before we dive deeper into creating APIs, let’s take a moment to set up our database. After all, our blogging platform needs a place to store all those blog posts, right?
We’ll use PostgreSQL as our database, and Encore makes it incredibly easy to integrate and manage.
Once the database is set up and ready to go, we’ll jump back into the API creation part. This way, we’ll have everything in place to not just create blogs but also store and retrieve them seamlessly.
Let’s get the database up and running first then we’ll continue building those APIs!
Connect with PostgreSQL
In order to connect with PostgreSQL, the first thing you will need is Docker running on your machine. Docker is great for creating and managing databases like PostgreSQL, preventing your local environment from getting too messy.
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.
Now that we have Docker, we’ll run PostgreSQL and then connect to our encore project. First, let’s get Docker!
Once Docker is up and running you don’t need to do anything else with it for now. It’s just there, doing its thing in the background.
Now, let’s focus on creating a database schema for our blog 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 blog 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 named1_create_blogs_table.up.sql
. The name might look a bit specific, but it’s just a convention the1
indicates it’s the first migration, and.up.sql
means this file will handle creating or updating the database schema.
In this file we’ll define the schema for our blogs
table.
Here’s an example:
CREATE TABLE blogs (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
author TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
This schema includes:
- An
id
column that auto-increments with each new blog post. -
title
,content
, andauthor
columns to store the blog details. - A
created_at
column to track when the blog was created.
Once this migration file is in place, Encore will automatically apply it to the database when we run the project.
And just like that, our database will be ready to store blog posts!
import { SQLDatabase } from "encore.dev/storage/sqldb";
Now, let’s connect our code to the database. 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.
Once we’ve imported it we’ll create a database instance. Since our database is named blogs
, 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.
Here’s how it looks in the code:
const db = new SQLDatabase("blogs", { migrations: "./migrations" });
With this setup, Encore knows exactly where to find the migration files and will automatically apply them to the blogs
database when we run the project.
It’s a clean and efficient way to manage our database schema as we build out the blogging platform.
import { api, APIError } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("blogs", { migrations: "./migrations" });
interface Blog {
id: number;
title: string;
content: string;
author: string;
created_at: string;
updated_at: string;
}
interface CreateBlogParams {
title: string;
content: string;
author: string;
}
// Create Blog
export const createBlog = api<CreateBlogParams, Blog>(
{
method: "POST",
path: "/blogs",
expose: true,
},
async ({ title, content, author }: CreateBlogParams): Promise<Blog> => {
const row = await db.queryRow<Blog>`
INSERT INTO blogs (title, content, author)
VALUES (${title}, ${content}, ${author})
RETURNING id, title, content, author, created_at, updated_at`;
if (!row) {
throw APIError.internal("Failed to create blog");
}
return row;
}
);
We’ve now used an SQL query to save the title
, content
, and author
into the blogs
database.
It’s a simple SQL command that handles storing data in the database. 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.
Just click on it, provide the required data, and hit send. You’ll instantly see the response whether it’s a success or an error.
It’s a quick and easy way to make sure everything’s working as expected. Let’s give it a try!
Now, when we check the response, we can see the id
, created_at
, and updated_at
fields. This means our data has been successfully saved in the database. 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 blogs
Here, blogs
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.
We can see the data, though it’s a bit cramped because of the limited space in the terminal. But the important thing is it’s working! The data is being saved correctly, and we’ve verified it.
And with that, we’ve successfully completed our very first endpoint for creating a blog. Cool, right?
Let’s keep this momentum going and build more!
Read All Blogs:
Now, let’s create our second endpoint this time to retrieve all the blogs from our database. Just like we did for the createBlog
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!
// Read All Blogs
export const getBlogs = api(
{
method: "GET",
path: "/blogs",
expose: true,
},
async (): Promise<{ blogs: Blog[] }> => {
const rows = db.query`
SELECT id, title, content, created_at
FROM blogs
ORDER BY created_at DESC
`;
const blogs: Blog[] = [];
for await (const row of rows) {
blogs.push({
id: row.id,
title: row.title,
content: row.content,
author: row.author,
created_at: row.created_at,
updated_at: row.updated_at,
});
}
return { blogs };
}
);
Let’s test our new endpoint and see if all the blogs are showing up correctly.
Head over to the Developer Dashboard, find the getAllBlogs
endpoint in the dropdown, and hit send.
If everything’s set up right, you should see a list of all the blogs stored in your database.
Get blog by ID:
Now, let’s create an endpoint to get a single blog 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 blog 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!
// Read a Single Blog by ID
export const getBlogById = api<{ id: number }, Blog>(
{
method: "GET",
path: "/blogs/:id",
expose: true,
},
async ({ id }: { id: number }): Promise<Blog> => {
const row = await db.queryRow<Blog>`
SELECT * FROM blogs WHERE id = ${id}`;
if (!row) {
throw APIError.notFound("Blog not found");
}
return row;
}
);
Let’s test this new endpoint using the Developer Dashboard. Since we only have one blog in the database so far, we’ll check for the blog with id: 1
.
Head over to the dashboard, find the getBlogById
endpoint, and pass 1
as the ID. Hit send, and you should see the details of that single blog pop up in the response.
Update blog by ID:
Updating a blog by its ID is pretty straightforward. First, we’ll define an UpdateBlogParams interface to structure the data we’re expecting like the id
, title
, content
, and author
.
Then, we’ll use an SQL query to update the blog in the database based on the provided id
.
If the blog with the given id
doesn’t exist, we’ll throw an error message to let the user know.
interface UpdateBlogParams {
id: number;
title: string;
content: string;
}
// Update Blog
export const updateBlog = api<UpdateBlogParams, Blog>(
{
method: "PUT",
path: "/blogs/:id",
expose: true,
},
async ({ id, title, content }: UpdateBlogParams): Promise<Blog> => {
const row = await db.queryRow<Blog>`
UPDATE blogs
SET title = ${title}, content = ${content}, updated_at = NOW()
WHERE id = ${id}
RETURNING id, title, content, author, created_at, updated_at`;
if (!row) {
throw APIError.notFound("Blog not found");
}
return row;
}
);
Let’s test this out and see how it works. Head over to the Developer Dashboard, find the updateBlog
endpoint, and pass in the id
of the blog you want to update along with the new title
, content
, or author
.
The title
and content
for blog 1 have been updated based on the data we sent in the request body.
If you check the response in the Developer Dashboard, you’ll see the changes reflected there.
Delete the blog by ID:
Now, let’s see how we can delete a blog by its ID. We’ll take the id
as a parameter in the function and execute a simple SQL query to remove the blog from the database.
If the blog with the given id
exists, it’ll be deleted. If not, we’ll handle it gracefully with an error message.
// Delete Blog
export const deleteBlog = api<{ id: number }, { message: string }>(
{
method: "DELETE",
path: "/blogs/:id",
expose: true,
},
async ({ id }: { id: number }): Promise<{ message: string }> => {
const row = await db.queryRow<Blog>`
SELECT * FROM blogs WHERE id = ${id}`;
if (!row) {
throw APIError.notFound("Blog not found");
}
await db.exec`DELETE FROM blogs WHERE id = ${id}`;
return { message: "Blog deleted successfully" };
}
);
Here’s the test version of the delete endpoint. Head over to the Developer Dashboard, find the deleteBlog
endpoint, and pass the id
of the blog you want to delete.
Now that we’ve walked through all the CRUD operations for our blog API Create, Read, Update, and Delete you’ve got the foundation to build a fully functional application.
From saving a new blog to fetching, updating, and even deleting it, we’ve covered the essentials.
I hope this gives you a solid understanding of how to create and manage an application using Encore. There’s always more to explore, but this is a great starting point.
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 create, read, update, and delete blogs.
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. Let’s take it for a spin!
API Integration with Frontend
I’ve created a couple of blogs to showcase on the frontend. Using the live link we got after deployment, I added 4 blogs to the database.
It’s exciting to see how quickly everything comes together from building the backend to populating it with real data.
Now, these blogs are ready to be displayed on the frontend.
Here, I have created a simple Next.js frontend app to demonstrate how our APIs are ready to integrate with any frontend.
Whether you’re building a sleek, modern UI or something more minimalist, the backend is fully prepared to handle it.
This is the beauty of a well-structured API, it works seamlessly with any frontend design you can imagine.
"use client";
import axios from "axios";
import { useEffect, useState } from "react";
export default function BlogList() {
const [blogs, setBlogs] = useState<
{
id: string;
title: string;
content: string;
author: string;
created_at: string;
}[]
>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchBlogs = async () => {
try {
const response = await axios.get(
"https://staging-blogs-nqei.encr.app/blogs"
);
console.log(response);
setBlogs(response?.data?.blogs);
} catch (err) {
console.log(err);
setError("Failed to load blogs. Please try again later.");
} finally {
setLoading(false);
}
};
fetchBlogs();
}, []);
if (loading) {
return (
<div className="text-center mt-20 text-lg text-gray-500">
Loading blogs...
</div>
);
}
if (error) {
return (
<div className="text-center mt-20 text-lg text-red-500">{error}</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-blue-100 py-10 px-4 sm:px-10">
<h1 className="text-4xl font-bold text-center text-gray-700 mb-10 drop-shadow-lg">
{`Syket's`} Blog
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{blogs.map((blog) => (
<div
key={blog.id}
className="bg-white shadow-md rounded-2xl p-6 hover:shadow-xl transition-shadow duration-300"
>
<h2 className="text-xl font-semibold text-gray-800 mb-2 truncate">
{blog.title}
</h2>
<p className="text-gray-600 text-sm line-clamp-3">{blog.content}</p>
</div>
))}
</div>
</div>
);
}
And here it is our little, simple blog page! It’s nothing fancy, but it gets the job done. You can see the blogs we created earlier displayed right here, fetched from our backend API.
It’s a great example of how everything ties together backend and frontend working in harmony.
Conclusion
It has been an incredible journey for us from the very beginning setup of the project to creating the first API, connecting it to the PostgreSQL database, and then deploying it in the Encore environment.
There was so much to do and to behold as the pieces all came together. Encore makes it easy to design robust, type-safe applications that can scale up quickly.
If you want a closer look, don't fail to check out the Documentation. There is plenty to do with Encore.
Also, if you like the project please Star Encore on GitHub.
Thanks for reading till the end!
Top comments (1)
Hi, I have been following Encore for years and love the Go framework. Isn't it an overkill platform for a blog?