Building and deploying a microservices application can be very challenging. This is usually because, with traditional frameworks, it requires a lot of work to set up and manage the necessary infrastructure. And once it is set up, you also need to make sure the services are working as expected, are secure and scalable, and can connect and communicate between each other.
Encore.ts is an Open Source TypeScript framework that solves these challenges and simplifies the entire process. It enables you to build robust distributed systems, where most of the heavy lifting is handled automatically.
In this tutorial, you’ll learn how to build a microservice with Encore.ts, and deploy it to a Kubernetes cluster in your AWS account. We’ll show you how Encore Cloud (Encore’s managed DevOps automation platform) automates the deployment process, handling everything from setting up the Kubernetes cluster and all necessary IAM policies and other resources, to deploying your microservices.
Prerequisites
- Encore installed locally on your computer (we'll do this in the next step)
- Docker (You need Docker to run Encore applications with databases locally.)
- Code Editor of your choice
The code for this tutorial is available here, feel free to clone and follow along.
Install Encore
Install the Encore CLI to run your local environment:
-
macOS:
brew install encoredev/tap/encore
-
Linux:
curl -L https://encore.dev/install.sh | bash
-
Windows:
iwr https://encore.dev/install.ps1 | iex
Creating a microservice with Encore
Let’s start by creating a microservice using Encore.ts. We’ll build a simple blog microservice to demonstrate how to deploy microservice applications using Encore.ts.
Create a New Application
Run the command to scaffold a new Encore.ts application:
encore app create blog-microservices
The above command will prompt you to select the language for your application and project template. Your selection should look like the screenshot below:
Now change directory into the project folder and run your app with the command:
cd blog-microservices && encore run
The above command will open up the API on your browser:
Now let’s look at the folder structure for our microservice application. Create the following folder structure in your project directory.
blog-microservices/
├── encore.app
├── posts-service/
│ ├── encore.service.ts // Service definition
│ ├── posts.ts // API endpoints
│ └── migrations/
│ └── 1_create_posts.up.sql // Database migration definition
└── comments-service/
├── encore.service.ts // Service definition
├── comments.ts // API endpoints
└── migrations/
└── 1_create_comments.up.sql // Database migration definition
In this project structure, we have two microservices, the posts-service
and comments-service
services. The posts-service
handles all post related logics and functions such as creating new posts, fetching posts, updating and deleting posts. While the comments-service
handles all the comments related operations. This way, your application is decompiled and allows you to easily manage each service independently.
Adding database integration
With the application and project files created, let’s proceed to creating a database and creating schema.
In your posts-service/posts.ts
file and add the following code to setup a database posts database:
import { SQLDatabase } from "encore.dev/storage/sqldb";
// Database setup
const db = new SQLDatabase("comments", {
migrations: "./migrations",
});
Then update yourposts-service/migrations/1_create_posts.up.sql
file, add the following code snippets to define a posts
schema:
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content TEXT NOT NULL,
author_name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
The above code will create a posts table with the following fields:
-
id
: A random generated ID to uniquely identify each posts -
title
: The title of each post. -
content
: The actual blog post. -
author_name
: Name of the user posting the blog. -
created_at
: Date and time the post was created.
Next, add the following code to your comments-service/posts.ts
file to create a comments database:
import { SQLDatabase } from "encore.dev/storage/sqldb";
// Database setup
const db = new SQLDatabase("posts", {
migrations: "./migrations",
});
Update the comments-service/migrations/1_create_comments.up.sql
to create a comments
schema:
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID NOT NULL,
content TEXT NOT NULL,
author_name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
);
The above code will create a comments table with the following fields:
-
id
: A random generated ID to uniquely identify each comment. -
post_id
: ID of the post commented on, which creates a reference to the posts table. -
content
: The actual comment on the post. -
author_name
: Name of the user commenting on the posts. -
created_at
: Date and time the post was created.
Writing a basic REST API service
Now update your posts-service/posts
file to create REST API services for the posts microservice. We’ll add do endpoints, to create, fetch all posts and get posts by ID from the database:
//...
import { api, Query } from "encore.dev/api";
import { Topic } from "encore.dev/pubsub";
//...
// Types
interface PostEvent {
id: string;
title: string;
authorName: string;
action: "created" | "updated" | "deleted";
}
export const postCreatedTopic = new Topic<PostEvent>("post-created", {
deliveryGuarantee: "at-least-once",
});
interface Post {
id: string;
title: string;
content: string;
authorName: string;
createdAt: Date;
}
interface CreatePostRequest {
title: string;
content: string;
authorName: string;
}
interface ListPostsRequest {
limit?: Query<number>;
offset?: Query<number>;
}
interface ListPostsResponse {
posts: Post[];
total: number;
}
// API Endpoints
export const createPost = api(
{
method: "POST",
path: "/posts",
expose: true,
},
async (req: CreatePostRequest): Promise<Post> => {
const post = await db.queryRow<Post>`
INSERT INTO posts (title, content, author_name)
VALUES (${req.title}, ${req.content}, ${req.authorName})
RETURNING
id,
title,
content,
author_name as "authorName",
created_at as "createdAt"
`;
await postCreatedTopic.publish({
id: post?.id as string,
title: post?.title as string,
authorName: post?.authorName as string,
action: "created",
});
return post as Post;
}
);
export const getPost = api(
{
method: "GET",
path: "/posts/:id",
expose: true,
},
async (params: { id: string }): Promise<Post> => {
return (await db.queryRow<Post>`
SELECT
id,
title,
content,
author_name as "authorName",
created_at as "createdAt"
FROM posts
WHERE id = ${params.id}
`) as Post;
}
);
export const listPosts = api(
{
method: "GET",
path: "/posts",
expose: true,
},
async (params: ListPostsRequest): Promise<ListPostsResponse> => {
const limit = params.limit || 10;
const offset = params.offset || 0;
// Get total count
const totalResult = await db.queryRow<{ count: string }>`
SELECT COUNT(*) as count FROM posts
`;
const total = parseInt(totalResult?.count || "0");
// Get paginated posts
const posts = await db.query<Post>`
SELECT
id,
title,
content,
author_name as "authorName",
created_at as "createdAt"
FROM posts
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const result: Post[] = [];
for await (const post of posts) {
result.push(post);
}
return {
posts: result,
total,
};
}
);
In the above code snippet, we defined a series of TypeScript interfaces and API routes that manages posts service. The PostEvent
interface is used to shape messages sent to a message topic (postCreatedTopic
), which records actions like post creation, updates, and deletions. The main Post
interface defines the structure of a blog post, while CreatePostRequest
, ListPostsRequest
, and ListPostsResponse
provide types for request and response handling in the service's API endpoints. We created three routes, createPost
which adds a new post to the database and publishes an event to the topic; getPost
, which retrieves a specific post by its ID; and listPosts
, which provides paginated post data.
Next, update the comments-service/posts
to create REST API services for the comments microservice:
//...
import { api, Query } from "encore.dev/api";
//...
// Types
interface Comment {
id: string;
postId: string;
content: string;
authorName: string;
createdAt: Date;
}
interface CreateCommentRequest {
postId: string;
content: string;
authorName: string;
}
interface ListCommentsRequest {
limit?: Query<number>;
offset?: Query<number>;
postId: string;
}
interface ListCommentsResponse {
comments: Comment[];
}
export const listComments = api(
{
method: "GET",
path: "/comments/:postId",
expose: true,
},
async (params: ListCommentsRequest): Promise<ListCommentsResponse> => {
const limit = params.limit || 10;
const offset = params.offset || 0;
const comments = await db.query<Comment>`
SELECT
id,
post_id as "postId",
content,
author_name as "authorName",
created_at as "createdAt"
FROM comments
WHERE post_id = ${params.postId}
ORDER BY created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const result: Comment[] = [];
for await (const comment of comments) {
result.push(comment);
}
return { comments: result };
}
);
In this code, we defined interfaces and API routes to manage comments on blog posts. The Comment
interface shapes the data structure for each comment, including fields like postId
and createdAt
. The CreateCommentRequest
, ListCommentsRequest
, and ListCommentsResponse
interfaces structure the requests and responses for our endpoints, ensuring type-safe interactions. We created a listComments
, which retrieves a paginated list of comments for a specified post.
Implementing service-to-service communication
First we need to define each microservice as a service in Encore.
To do this for the posts
service, add this code to the posts-service/encore.service.ts
file:
import { Service } from "encore.dev/service";
export default new Service("posts");
Then for the comments
service, add this code to the comments-service/encore.service.ts
file to create a comments service:
import { Service } from "encore.dev/service";
export default new Service("comments");
Next, to call the endpoints in the posts
service from the comments
service, simply import it in the comments
service, like so:
import { posts } from "~encore/clients";
Now you can all its endpoints like normal functions from the comments
service, like so:
// API Endpoints
export const createComment = api(
{
method: "POST",
path: "/comments",
expose: true,
},
async (req: CreateCommentRequest): Promise<Comment> => {
// Verify post exists
const post = await posts.getPost({ id: req.postId as string });
if (!post) {
throw new Error("Post not found");
}
return (await db.queryRow<Comment>`
INSERT INTO comments (post_id, content, author_name)
VALUES (${req.postId}, ${req.content}, ${req.authorName})
RETURNING
id,
post_id as "postId",
content,
author_name as "authorName",
created_at as "createdAt"
`) as Comment;
}
);
Here, we created a createComment
, which allows users to add a comment to an existing post (after verifying the post exists) we imported the posts services and used it to access the getPost
method which checks if the posts the user is trying to comment on exists.
Testing the service locally
Now let’s test the microservice API routes. Go back to your API explorer to test your endpoints:
Deploying to Kubernates
Now that you have your microservices application up and running, let’s use Encore Cloud (Encore’s managed service for DevOps automation) to automatically deploy it to a Kubernetes cluster in your AWS account.
We’ll be deploying your microservice to a new Kubernetes cluster. You can find the guide on deploying to an existing Kubernetes cluster here.
Run the command below to deploying the application:
git add -A .
git commit -m 'first deploy'
git push encore
Connecting your cloud account:
The first step in deploying your Encore.ts microservice to a Kubernetes cluster is connecting your cloud account, such as AWS or GCP, to your app in Encore Cloud. In this tutorial, we’ll use the Amazon Web Service(AWS) as an example. Follow the steps below to connect your AWS account to your app in Encore Cloud:
- From your Encore Cloud dashboard, navigate to:
- Select your app
- Go to App Settings > Integrations > Connect Cloud
- Login to your AWS account and create a new IAM Role with the following steps:
- Go to the Create Role page in the Identity and Access Management (IAM) console.
- Select Another AWS Account as the account type.
- Copy and paste your Account ID from Encore Cloud.
- Check the Require external ID option.
- Copy your External ID from **Encore Cloud and paste in the **External ID field in AWS.
- Attach the AdministratorAccess permission policy to the role (required by Encore to provision resources on your behalf).
- Enter role name, description and click the Create Role button.
- Paste your AWS Role ARN and click the Continue button to connect your Encore Cloud to AWS.
Creating environment
Now that you have connected AWS console to your Encore.ts Cloud, let’s proceed to creating an new Enviroment to deploy the Microservice to Kubernates. To do that follow the steps below:
- Open your app in the Encore Cloud dashboard and go to Environments and click Create Environment.
- Select your cloud and compute platform.
- Choose AWS as your cloud provider.
- Specify Kubernetes as the compute platform (Encore supports GKE on GCP and EKS Fargate on AWS).
- Decide whether to allocate all services in a single process or run one process per service.
Once you have those configurations in place, click the Create button to create your new environment. Encore will provision and deploy the infrastructure on Kubernetes based on your environment configuration.
While your application is deploying you can monitor deployment status and environment details in your Encore Cloud dashboard. Also you can use the kubectl
CLI tool to access your Kubernetes cluster.
Wrapping up
In this tutorial, you’ve learned how to build and deploy your Encore.ts microservice application to Kubernetes.
We started by understanding what Encore.ts is, and how it solves the challenges faced by developers when building and deploying microservices.
Then we built a microservice application and deployed it to a Kubernetes cluster on AWS using Encore Cloud.
Now that you know how it works, perhaps you can try adding more features to the app.
Related links:
- Check out the Encore docs for more information on building applications with Encore.
- Feel free to Star Encore on GitHub
- Check out Encore's YouTube channel for video tutorials
Top comments (0)