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
- Klotho CLI installed
- Node.js 16.x+ (& NPM)
curl
- Docker
- Pulumi CLI installed
- Pulumi configured for AWS
- Pre-configured AWS API Gateway Custom Domain
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
.
...
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.
...
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 ('*'
).
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.
/**
* @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.
/**
* @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.
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:
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.
...
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.
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.
Finally, the page will refresh the list of displayed posts by invoking the application's API to retrieve all existing posts from the database.
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