Skip to main content

Building a cloud-native Full-Stack NextJS + TypeORM application with Klotho

Overview

This tutorial shows how to transform a full-stack web application built with NextJS and TypeORM into a cloud application ready for deployment.

Getting Started

Prerequisites

Repository

Clone our sample apps git repo and install the npm packages for the ts-nextjs-typeorm application.

git clone https://github.com/klothoplatform/sample-apps.git
cd sample-apps/ts-nextjs-typeorm
npm install

Application Overview

The ts-nextjs-typeorm sample application discussed in this tutorial is a simple blogging website that allows visitors to submit new markdown-formatted posts and view all previously submitted posts. The static content and API endpoints are serverd using NextJS and the posts are stored in a SQL database that the application interacts with via TypeORM.

This application utilizes the following annotations:

Wiring the Application for Klotho Compilation

Configuring a custom NextJS Server

In order to take advantage of Klotho's existing support for Express, the application configures a custom NextJS server. This custom server is then used to handle all requests to an Express app.

First, the application initializes a new instance of NextServer and prepares its RequestHandler.

next-app.ts
...
import next from "next";
import { datasource } from "./datasource";

const dev = process.env.NODE_ENV != 'production' && !process.env.CLOUDCC_NAMESPACE
const app = next({ dev });
const handle = app.getRequestHandler();
const prepared = app.prepare();

The application then defines an Express handler function that delegates incoming requests to the NextJS RequestHandler initialized above.

next-app.ts
...
const handle = app.getRequestHandler();
...
export async function nextHandler(req, res) {
...
return handle(req, res); // handles the request with NextJS
}

To handle all requests to the application with NextJS, the application mounts nextHandler on an Express app, server, using a catch-all route by invoking server.all and supplying a wildcard path ('*').

server.ts
const express = require("express");
import { nextHandler } from "./next-app";

const server = express();

server.all('*', nextHandler);
...

The application then starts the Express app by invoking server.listen.

Annotating this server.listen invocation with @klotho::expose lets Klotho know that this app should be exposed to the internet using an API Gateway.

server.ts
/**
* @klotho::expose {
* id = "NextGateway"
* target = "public"
* }
*/
server.listen(3000, () => console.log("> Ready on http://localhost:3000"));

Bundling Static Assets

To ensure that Klotho is including any required static content in the application's execution unit(s), it's important to use the @klotho::embed_assets annotation and define the appropriate file inclusion patterns.

For NextJS, the public/** and .next/** patterns ensure that Klotho will include any static assets required to run the application.

next-app.ts
/**
* @klotho::execution_unit {
* id = "NextAPI"
* }
*/

/**
* @klotho::embed_assets {
* id = "StaticContent"
* include = ["public/**", ".next/**"]
* }
*/
...

Data Persistence with TypeORM

Annotating a new TypeORM DataSource with @klotho::persist informs the Klotho runtime to configure the DataSource instance to connect to a cloud-hosted SQL database when the application is running in a cloud environment.

datasource.ts
import "reflect-metadata"
import { DataSource } from "typeorm";
import { Post } from "./post";

/**
* @klotho::persist {
* id = "PostsDB"
* }
*/
export const datasource = new DataSource({
type: "sqlite",
database: "posts.sqlite",
synchronize: true,
logging: true,
entities: [Post],
})
...

Compiling the Application with Klotho

Start by compiling the TypeScript application into JavaScript.

npx tsc

The JavaScript output will be located in the ./dist directory.

Then build the NextJS application.

npx next build

The compiled output will be located in the ./.next directory.

Finally, compile the application with Klotho.

klotho . --app ts-nextjs-typeorm --provider aws
██╗  ██╗██╗      ██████╗ ████████╗██╗  ██╗ ██████╗
██║ ██╔╝██║ ██╔═══██╗╚══██╔══╝██║ ██║██╔═══██╗
█████╔╝ ██║ ██║ ██║ ██║ ███████║██║ ██║
██╔═██╗ ██║ ██║ ██║ ██║ ██╔══██║██║ ██║
██║ ██╗███████╗╚██████╔╝ ██║ ██║ ██║╚██████╔╝
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝

Adding resource input_file_dependencies:
Adding resource exec_unit:NextAPI
src/next-app.js:17:0: Adding asset '.next/...' to unit 'NextAPI'
...
Found 2 route(s) on server 'server'
Adding resource gateway:NextGateway
Adding resource persist_orm:PostsDB
Adding resource topology:ts-nextjs-typeorm
Adding resource infra_as_code:Pulumi (AWS)

In the ./compiled folder, you will find the Infrastructure-as-Code (IaC) for your cloud-native application ready for deployment. You will also find a file called ts-nextjs-typeorm.png, visualizing the topology output of your compiled application:

topology diagram showing the NextGateway API Gateway invoking the NextAPI Lambda which interacts with the PostsDB RDS instance

The topology diagram shows that Klotho has generated IaC targeting AWS to deploy an API Gateway, NextGateway, that invokes the NextAPI Lambda function, which writes to and reads from the PostsDB RDS instance.

Examining the persisted section of the Klotho.yaml file generated by Klotho (./compiled/Klotho.yaml), you will see that Postgres will be used as the relational database engine for the Klotho-compiled version of the application.

Klotho.yaml
...
persisted:
PostsDB:
type: rds_postgres
...

Deploying the Application

Once the IaC has been generated, you can deploy the cloud-native version of the application with Pulumi.

First, use the Pulumi CLI to set the region aws:region setting for the application's Pulumi stack. This is the AWS region that Pulumi will deploy the application to.

pulumi config set aws:region <region> --cwd './compiled' --stack ts-nextjs-typeorm

Press ENTER () to confirm that you want to create the ts-nextjs-typeorm Pulumi stack.

If you would like to create this stack now, please press <ENTER>, otherwise press ^C:
Created stack 'ts-nextjs-typeorm'

Next, change the current working directory to ./compiled and install the dependencies of the Pulumi application generated by Klotho.

cd compiled
npm install

Then deploy the application by running pulumi up.

pulumi up

Pulumi will display a preview of all the cloud resources it will create as part of the deployment.

Previewing update (ts-nextjs-typeorm)

Type Name Plan
+ pulumi:pulumi:Stack ts-nextjs-typeorm create...
+ ├─ awsx:x:ec2:Vpc ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ ├─ awsx:ecr:Repository ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ └─ aws:ecr:LifecyclePolicy ts-nextjs-typeorm create
+ │ │ └─ aws:ec2:RouteTableAssociation ts-nextjs-typeorm-public-1 create
+ ├─ aws:iam:Role postsdb-ormsecretrole create
+ │ └─ aws:ec2:RouteTableAssociation ts-nextjs-typeorm-private-1 create
+ ├─ aws:apigateway:RestApi NextGateway create
+ ├─ aws:apigateway:RestApi NextGateway create
+ ├─ aws:apigateway:RestApi NextGateway create
+ ├─ aws:apigateway:RestApi NextGateway create
+ ├─ aws:apigateway:RestApi NextGateway create
+ │ │ └─ aws:ec2:NatGateway ts-nextjs-typeorm-1 create
+ │ │ └─ aws:apigateway:Method ANY-rest-3e9f5 create
+ │ └─ aws:apigateway:Method ANY-/-8a5ed create
+ ├─ aws:ecr:Repository ts-nextjs-typeorm create
+ ├─ aws:secretsmanager:Secret postsdb_secret create
+ ├─ aws:secretsmanager:Secret postsdb_secret create
+ ├─ aws:secretsmanager:Secret postsdb_secret create
+ ├─ aws:secretsmanager:Secret postsdb_secret create
+ │ │ └─ aws:ec2:Route ts-nextjs-typeorm-private-0-nat-0 create
+ ├─ aws:iam:RolePolicyAttachment ts-nextjs-typeorm-NextAPI-lambdabasic create
+ ├─ aws:s3:Bucket create
+ ├─ aws:ec2:SecurityGroup ts-nextjs-typeorm create
+ ├─ aws:ec2:SecurityGroupRule ts-nextjs-typeorm-ingress create
+ ├─ aws:ec2:VpcEndpoint s3VpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint dynamodbVpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint secretsmanagerVpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint secretsmanagerVpcEndpoint create
+ │ └─ aws:apigateway:Integration lambda-ANY-/-8a5ed create
+ ├─ aws:ec2:VpcEndpoint lambdaVpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint lambdaVpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint lambdaVpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint lambdaVpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint lambdaVpcEndpoint create
+ ├─ aws:ec2:VpcEndpoint lambdaVpcEndpoint create
+ pulumi:pulumi:Stack ts-nextjs-typeorm create
+ ├─ aws:ec2:VpcEndpoint sqsVpcEndpoint create
+ ├─ aws:rds:Instance postsdb create
+ ├─ aws:secretsmanager:SecretVersion postsdb_secret create
+ ├─ aws:rds:Proxy postsdb create
+ ├─ aws:iam:Policy postsdb-ormsecretpolicy create
+ ├─ aws:iam:RolePolicyAttachment postsdb-ormattach create
+ ├─ aws:rds:ProxyDefaultTargetGroup postsdb create
+ ├─ aws:lambda:Function NextAPI create
+ ├─ aws:rds:ProxyTarget postsdb create
+ ├─ aws:lambda:Permission any-rest-permission create
+ └─ aws:lambda:Permission any--permission create

Outputs:
apiUrls : [
[0]: output<string>
]
deploymentPortal: "None - Opted out of topology upload by default"

Resources:
+ 69 to create


Do you want to perform this update? [Use arrows to move, enter to select, type to filter]
> yes
no
details

Select yes from the displayed options to start the deployment process and then wait until Pulumi has completed the deploying the application's stack. The initial deployment usually takes 8-10 minutes as database creation may take several minutes.

When Pulumi has finished deploying, you will see the completion status and the AWS provided API Gateway URL for your API:

Outputs:
apiUrls : [
[0]: "https://<gateway_id>.execute-api.<region>.amazonaws.com/stage"
]
deploymentPortal: "None - Opted out of topology upload by default"
Resources:
+ 69 created
Duration: 9m45s

Attaching a Custom Domain

For the application to be able to correctly route to any API endpoints or static embedded static content, you will need to map the NextGateway API Gateway to an existing custom domain.

Check out our TodoMVC tutorial for an example of creating and attaching an AWS Route53 domain to an API Gateway.

info

Support for attaching custom domains to with Klotho and Pulumi is coming soon.

Preview: Attaching a Domain to an API Gateway

Testing the Deployed Application

Once the application is deployed and your custom domain has been attached, visit your custom domain in your browser.

The first time the application's homepage loads, there will be no posts present.

Click the NEW POST button in the top-right corner of the page to open the post submission form.

The page displays no posts

Now you can enter the post's title, author and content. The content will be formatted using Markdown.

Once you have finished writing your first post, click the SUBMIT button to save your post to the cloud-hosted database using the application's API.

Submit a new post

Finally, the page will refresh the list of displayed posts by invoking the application's API to retrieve all existing posts from the database.

New post is displayed as markdown formatted content

Cleanup

When you're done with the tutorial, you can destroy the created resources by running pulumi destroy from the ./compiled directory and selecting yes when prompted.

pulumi destroy