DEV Community

Cover image for Building a personal blogging platform with Next.js and Encore.ts
Marcus Kohlberg for Encore

Posted on

Building a personal blogging platform with Next.js and Encore.ts

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?

Image

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.

Image

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Image

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
Enter fullscreen mode Exit fullscreen mode

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.

Image

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.

Image

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.

Image description

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.

Image

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!

Image

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
Enter fullscreen mode Exit fullscreen mode

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!

Image

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.

Image

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:

  1. An object as the first parameter, where we define the method (like GET, POST, etc) the path (like /hello), and a key called expose: true. Setting expose: true means this endpoint is publicly accessible anyone can call it.
  2. 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
);
Enter fullscreen mode Exit fullscreen mode

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.

Image

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!

Image

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:

  1. 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.
  2. Inside the migrations folder, we’ll create a file named 1_create_blogs_table.up.sql. The name might look a bit specific, but it’s just a convention the 1 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
);
Enter fullscreen mode Exit fullscreen mode

This schema includes:

  • An id column that auto-increments with each new blog post.
  • titlecontent, and author columns to store the blog details.
  • 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";
Enter fullscreen mode Exit fullscreen mode

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" });
Enter fullscreen mode Exit fullscreen mode

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;
  }
);
Enter fullscreen mode Exit fullscreen mode

We’ve now used an SQL query to save the titlecontent, 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.

Image

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!

Image

Now, when we check the response, we can see the idcreated_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
Enter fullscreen mode Exit fullscreen mode

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!

Image

Now we can query from our table.

Image

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 };
  }
);
Enter fullscreen mode Exit fullscreen mode

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.

Image

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;
  }
);
Enter fullscreen mode Exit fullscreen mode

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. 

Image

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 idtitlecontent, 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;
  }
);
Enter fullscreen mode Exit fullscreen mode

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 titlecontent, or author

Image

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" };
  }
);
Enter fullscreen mode Exit fullscreen mode

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.

Image

Now that we’ve walked through all the CRUD operations for our blog API CreateReadUpdate, 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:

  1. Stage your changes:

    git add .
    
  2. Commit your changes with a message:

    git commit -m "Your commit message here"
    
  3. 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.

Image

Now open your app from app.encore.dev

Image

Here’s your live hosted URL! You can now use this URL to createreadupdate, 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.

Postman

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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.

Demo

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)

Collapse
 
sbusso profile image
Stéphane Busso

Hi, I have been following Encore for years and love the Go framework. Isn't it an overkill platform for a blog?