logoStacktape docs


Express.js API with Postgres

Intro

  • You will create a simple Node.js HTTP API with 2 endpoints:

    • POST /post to create a new post
    • GET /posts to get all the existing posts
  • Your application code will run in a fully managed, scalable and self-healing container workload.

  • The data will be stored in a Postgres database.

  • To simplify database access and migrations, this tutorial uses Prisma (next-gen ORM).

  • This tutorial uses YAML to write the configuration, but you can also use javascript or typescript.

Pricing

Two resources will incur costs (least expensive options are used):

  • Container workload (~$0.012/hour, ~$9/month)
  • Relational (SQL) database ($0.017/hour, ~$12.5/month, free-tier eligible)

There are also other resources that might incur costs (with pay-per-use pricing). If your load won't get high, the costs will be close to 0. You can also control your cloud spend using budget control.

1. Configure your stack

Start with an empty project directory. Call it whatever you like.


To deploy your application, you need to configure your "stack". The stack consists of an application code and the infrastructure required to run it. You can deploy your stack to multiple environments (stages) - for example production, staging or testing.

1.1 Add a configuration file

To configure the stack, Stacktape requires you to write a simple configuration.

Start by creating a file called stacktape.yml in the root directory of your project. In this tutorial, you will use YAML to write your configuration, but Stacktape supports more options (JSON, Javascript and Typescript).

If you're using vscode, you can install Stacktape extension. It automatically validates your configuration file, provides documentation (when you hover over properties), autocompletion, suggestions and useful links.

1.2 Configure service name

You can choose an arbitrary name, for example posts-api-pg.

Copy

serviceName: posts-api-pg

1.3 Create infrastructure resources

This project requires 3 stacktape resources:


Your resources can have an arbitrary alphanumeric names (A-z0-9).

1.3.1 Add HTTP API Gateway

First, configure an HTTP API Gateway and call it mainApiGateway. You can also allow CORS.


Copy

serviceName: posts-api-pg
resources:
mainApiGateway:
type: http-api-gateway
properties:
cors:
enabled: true

1.3.2 Add relational database

Now, add a relational-database (SQL database) resource. You need to configure a few things:

  • Database credentials. For now, input them directly. For production workloads, you should use secrets to store them securely.

  • Engine type. Use postgres engine. It uses a single-node database server - simplest and cheapest option.

  • Instance size. Use the db.t2.micro instance. It has 1 vCPU, 1GB of memory and is free-tier eligible (~$12.5/month without a free tier). To see the full list of available options, refer to AWS instance type list.


By default, the version used for your database is the latest AWS-supported stable version (currently 13.4). Minor version upgrades are done automatically.

You can also configure many other aspects of your database, such as storage, logging, read replicas, or failover instances.

Copy

serviceName: posts-api-pg
resources:
mainApiGateway:
type: http-api-gateway
properties:
cors:
enabled: true
mainDatabase:
type: relational-database
properties:
credentials:
masterUserName: admin_user
masterUserPassword: my_secret_password
engine:
type: postgres
properties:
primaryInstance:
instanceSize: db.t2.micro

1.3.3 Add container workload

Finally, add a container workload that will run your code. You need to configure several things:

  • Container. This container workload will use only a single container: api-container. You need to configure 3 things:
    • Packaging - determines how the Docker container image will be built. The easiest and most optimized way to build the image from a Typescript application is using stacktape-image-buildpack. You only need to configure entryfilePath. Stacktape will automatically transpile and build your application code with all of its dependencies, build the Docker image, and push it to a pre-created image repository on AWS. You can also use other types of packaging.
    • Database connection string - you can pass it to the container as an environment variable. You can easily reference it using a $ResourceParam() directive. This directive accepts a resource name (mainDatabase in this case) and the name of the relational database referenceable parameter (connectionString in this case). If you want to learn more, refer to referencing parameters guide and directives guide.
    • Events that will be able to reach your container. By configuring path to /{proxy+}, method to '*' and containerPort to 3000, the event integration will route all requests (no matter the method or path) coming to the HTTP Api Gateway to port 3000 of your container.
  • Resources. The cheapest available configuration is 0.25 of virtual CPU and 512 MB of RAM.

You can also configure scaling. New (parallel) container workload instance can be added when the utilization of your CPU or RAM gets larger than 80%. The HTTP API Gateway will evenly distribute the traffic to all container workloads.


Copy

serviceName: posts-api-pg
resources:
mainApiGateway:
type: http-api-gateway
properties:
cors:
enabled: true
mainDatabase:
type: relational-database
properties:
credentials:
masterUserName: admin_user
masterUserPassword: my_secret_password
engine:
type: postgres
properties:
primaryInstance:
instanceSize: db.t2.micro
apiServer:
type: container-workload
properties:
resources:
cpu: 0.25
memory: 512
containers:
- name: api-container
packaging:
type: stacktape-image-buildpack
properties:
entryfilePath: ./src/index.ts
environment:
- name: DB_CONNECTION_STRING
value: $ResourceParam('mainDatabase', 'connectionString')
- name: PORT
value: 3000
events:
- type: http-api-gateway
properties:
containerPort: 3000
httpApiGatewayName: mainApiGateway
method: "*"
path: /{proxy+}

1.4 Add Prisma

To simplify database access and migrations, you can use Prisma. If you're not familiar with it, don't worry - it's very simple.



First, initialize your project directory as a javascript project.

Copy

npm init

Next, install 2 packages: @prisma/client and prisma.

Copy

npm install @prisma/client prisma

or

Copy

yarn add @prisma/client prisma


Now, let's create a Prisma schema at prisma/schema.prisma in your project directory.

The schema declares a single model called Post.

It also configures prisma to use database specified by its url (taken from the DB_CONNECTION_STRING environment variable in our case). This database url will be used for both migrations and queries.

Copy

generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl"]
}
datasource db {
provider = "postgres"
url = env("DB_CONNECTION_STRING")
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @unique
content String
authorEmail String
}

Prisma generates a database client that you can then import from your code. To generate it, use the npx prisma generate command. To do it automatically every time before the stack is deployed, you can save the command as a script and then use it inside a before:deploy hook.

Copy

serviceName: posts-api-pg
scripts:
generatePrismaClient:
executeCommand: npx prisma generate
hooks:
- triggers: ["before:deploy"]
scriptName: generatePrismaClient
resources:
mainApiGateway:
type: http-api-gateway
properties:
cors:
enabled: true
mainDatabase:
type: relational-database
properties:
credentials:
masterUserName: admin_user
masterUserPassword: my_secret_password
engine:
type: postgres
properties:
primaryInstance:
instanceSize: db.t2.micro
apiServer:
type: container-workload
properties:
resources:
cpu: 0.25
memory: 512
containers:
- name: api-container
packaging:
type: stacktape-image-buildpack
properties:
entryfilePath: ./src/index.ts
environment:
- name: DB_CONNECTION_STRING
value: $ResourceParam('mainDatabase', 'connectionString')
- name: PORT
value: 3000
events:
- type: http-api-gateway
properties:
containerPort: 3000
httpApiGatewayName: mainApiGateway
method: "*"
path: /{proxy+}


To sync your Prisma schema with the database, you can use npx prisma db push command. You can't do this before the database is created, so you should use the after:deploy hook.


You also need to pass the DB_CONNECTION_STRING environment variable to the script. You can do it using the $ResourceParam() directive that will automatically download the connection string value and pass it to the script.

Copy

serviceName: posts-api-pg
scripts:
generatePrismaClient:
executeCommand: npx prisma generate
migrateDb:
executeCommand: npx prisma db push --skip-generate
environment:
- name: DB_CONNECTION_STRING
value: $ResourceParam('mainDatabase', 'connectionString')
hooks:
- triggers: ["before:deploy"]
scriptName: generatePrismaClient
- triggers: ["after:deploy"]
scriptName: migrateDb
resources:
mainApiGateway:
type: http-api-gateway
properties:
cors:
enabled: true
mainDatabase:
type: relational-database
properties:
credentials:
masterUserName: admin_user
masterUserPassword: my_secret_password
engine:
type: postgres
properties:
primaryInstance:
instanceSize: db.t2.micro
apiServer:
type: container-workload
properties:
resources:
cpu: 0.25
memory: 512
containers:
- name: api-container
packaging:
type: stacktape-image-buildpack
properties:
entryfilePath: ./src/index.ts
environment:
- name: DB_CONNECTION_STRING
value: $ResourceParam('mainDatabase', 'connectionString')
- name: PORT
value: 3000
events:
- type: http-api-gateway
properties:
containerPort: 3000
httpApiGatewayName: mainApiGateway
method: "*"
path: /{proxy+}

2. Write application code

2.1 Install dependencies

First we need to install some dependencies.

Copy

npm install express body-parser @types/express

or

Copy

yarn add express body-parser @types/express

2.2 Api-container application code

Create a new typescript file at ./src/index.ts. For simplicity, let's put everything into a single file.

Copy

import express from "express";
import { json } from "body-parser";
import { PrismaClient } from "@prisma/client";
// Instantiate the Prisma client.
const prisma = new PrismaClient();
const app = express();
app.use(json());
app.get("/posts", async (req, res) => {
try {
const posts = await prisma.post.findMany();
res.send({ message: "success", data: posts });
} catch (error) {
// If anything goes wrong, log the error.
// You can later access the log data in the AWS console.
console.error(error);
res.status(400).send({ message: "error", error: error.message });
}
});
app.post("/post", async (req, res) => {
try {
// save post data to the database using the Prisma client
const postData = await prisma.post.create({
data: {
title: req.body.title,
content: req.body.content,
authorEmail: req.body.authorEmail
}
});
res.send({ message: "success", data: postData });
} catch (error) {
console.error(error);
res.status(400).send({ message: "error", error: error.message });
}
});
app.listen(process.env.PORT, () => {
console.info(`Api container started. Listening on port ${process.env.PORT}`);
});


You don't need to configure anything else. Stacktape will automatically transpile Typescript and bundle your source code with all of the required dependencies.

3. Deployment

To deploy your application (stack), all you need is a single command:

Copy

stacktape deploy --stage <<stage>> --region <<region>>

  • stage is an arbitrary name of your environment (for example staging, production or dev-john)
  • region is the AWS region, where your stack will be deployed to. All the available regions are listed below.

Region name & Locationcode
Europe (Ireland)eu-west-1
Europe (London)eu-west-2
Europe (Frankfurt)eu-central-1
Europe (Milan)eu-south-1
Europe (Paris)eu-west-3
Europe (Stockholm)eu-north-1
US East (Ohio)us-east-2
US East (N. Virginia)us-east-1
US West (N. California)us-west-1
US West (Oregon)us-west-2
Canada (Central)ca-central-1
Africa (Cape Town)af-south-1
Asia Pacific (Hong Kong)ap-east-1
Asia Pacific (Mumbai)ap-south-1
Asia Pacific (Osaka-Local)ap-northeast-3
Asia Pacific (Seoul)ap-northeast-2
Asia Pacific (Singapore)ap-southeast-1
Asia Pacific (Sydney)ap-southeast-2
Asia Pacific (Tokyo)ap-northeast-1
China (Beijing)cn-north-1
China (Ningxia)cn-northwest-1
Middle East (Bahrain)me-south-1
South America (São Paulo)sa-east-1


The deployment progress is printed to the terminal.

The deployment will take ~20 minutes. Subsequent deploys will be way faster.

3.1 Deploying only source code

Deployment using the deploy command uses AWS CloudFormation under the hood.

This gives a lot of guarantees and convenience, but can be slow for certain use-cases.

If you only want to deploy your source code (not any configuration changes), you can use cw:deploy-fast. The deployment will be significantly faster (~60-120 seconds).

Copy

stacktape cw:deploy-fast --stage <<stage>> --region <<region>> --resourceName nextJsApp

4. After deployment

4.1 Explore your application

If the deployment is successful, some information about the deployed stack will be printed to the console. For instance:

  • stack URL is a link to AWS CloudFormation console of your stack. As you can see, more than 40 resources are required to run just a simple API.
  • apiServer -> logs-api-container is a link to AWS console where you can see logs produced by your container.
  • mainDatabase -> logs is a link to AWS console where you can see the logs produced by your database.
  • mainApiGateway -> logs is a link to AWS console where you can see the API Gateway access logs.
  • mainApiGateway -> url is the URL of the deployed HTTP API Gateway. Save this, you will need it in the next step.

You will also see links to metrics of your resources (such as CPU/memory usage of your database, etc.).


Furthermore, detailed information about the stack and deployed resources is saved to .stacktape-stack-info/<<stack-name>>.

4.2 Test the application


  • Make a POST request to <<your_http_api_gateway_url>>/post with the JSON data in its body to save the post. You can use a the following cURL commands (replace <<your_http_api_gateway_url>> with your mainApiGateway -> url):

    Copy

    curl -X POST <<your_http_api_gateway_url>>/post -H 'content-type: application/json' -d '{ "title": "MyPost", "content": "Hello!", "authorEmail": "info@stacktape.com"}'

    If the above command did not work, try escaping the JSON content, or use your preferred HTTP client.

    Copy

    curl -X POST <<your_http_api_gateway_url>>/post -H 'content-type: application/json' -d '{ \"title\":\"MyPost\",\"content\":\"Hello!\",\"authorEmail\":\"info@stacktape.com\"}'


  • Make a GET request to <<your_http_api_gateway_url>>/posts to fetch all posts.

    Copy

    curl <<your_http_api_gateway_url>>/posts

5. Next steps

5. a) Iteratively develop your application code

Stacktape allows you to easily develop your application in a "development" mode.


To develop a container workload, you can use the cw:run-local command. For example, to develop and debug container api-container within apiServer container workload, you can use

Copy

stacktape cw:run-local --region eu-west-1 --stage <<your-previously-deployed-stage>> --resourceName apiServer --container api-container

This will run the container locally, map all of the container ports specified in the events section to the host machine, and pretty-print logs produced by the container.


Stacktape emulates the workload as closely to the deployed version as possible. It does 2 things:

  • Injects parameters referenced in the environment variables by $ResourceParam and $Secret directives to the workload.
  • Injects credentials of the assumed role to the workload. This means that your locally running container will have the exact same IAM permissions as the deployed version.

The container is rebuilt and restarted, when you either:

  • type rs + enter to the terminal
  • use the --watch option and one of your source code files changes

5. b) Evolve your stack

Stacktape allows you to do much more things with your stack. For instance:

  • Add a CDN to cache GET requests coming to your HTTP API Gateway at an edge location. This can decrease load on your API and improve response times.
  • Create a CI/CD pipeline to deploy your application.
  • Add a custom domain name to your HTTP API Gateway.
  • Add a dynamic behavior to your configuration file using a custom directive. You can write it using Javascript, Typescript or Python.
  • Add a storage bucket to store files.
  • Add a Redis cluster to cache requests to the database. It can improve the response times and decrease the load on your database.
  • Use a different relational database engine. For instance, you can use one of aurora engines to increase performance, reliability and fault-tolerance.
  • Store your sensitive data (such as database credentials) as secrets.
  • Add a userpool to store, authenticate and authorize users.
  • Add a policy. For instance, you can allow only 2 stages for your application: staging and production.
  • Use a completely different database type. For instance DynamoDb or MongoDb.
  • Add a progress notification to get notified in Slack or MS Teams when the deployment finishes or fails.

5. c) Delete your stack

If you no longer want to use your stack, you can delete it. Stacktape will automatically delete every resource and deployment artifact associated with your stack.


Copy

stacktape delete --stage <<stage>> --region <<region>>
Need help? Ask a question on SlackDiscord or info@stacktape.com.