/aws-serverless-fullstack

A template to create a low-management, autoscaling NodeJS api + database on AWS.

Primary LanguageTypeScript

☠️ AWS-SERVERLESS-FULLSTACK ☠️

...is a template to create a low-management, autoscaling NodeJS api + database on AWS

What makes it cool:

  1. 100% serverless full-stack with Static frontend, NodeJS api and MySQL db
  2. Easy deploy management with AWS SAM + Cloudformation
  3. Run locally, AWS, or anywhere with generic file and db connectors
  4. All Javascript/Typescript
  5. Uses Fastify framework, which is mucho faster than Express
  6. DB ORM and schema management via Typeorm
  7. Type-strict

Table of Contents 🌍

Running locally 🏎

To run locally, ensure you have docker running and node v14.

npm i
npm run db # Start the database in Docker as a daemon
npm run dev # Start the API on https://0.0.0.0:3000

API

GET /

Serves static website in src/html

curl https://0.0.0.0:3000

GET /api/dbtime

Gets the current time from DB

curl https://0.0.0.0:3000/api/dbtime

POST /files/:id

Uploads a file

curl -X POST https://0.0.0.0:3000/files/1234 --form file=@image.jpg

GET /files/:id

Returns a file

curl https://0.0.0.0:3000/files/1234

GET /files/:id/meta

Returns a file's metadata

curl https://0.0.0.0:3000/files/1234/meta -H 'accept: application/json'

POST /api/users

Creates a user

curl -X POST \
  --url https://0.0.0.0:3000/api/users \
  --header 'Content-Type: application/json' \
  --data '{
		"email": "editor3@example.com",
		"roles": [1],
		"givenName": "Sally",
		"surname": "Editor",
		"password": "Yousuck8"
	}'

GET /api/users

Gets users

curl https://0.0.0.0:3000/api/users

DB Schema Management 🗂

You needs in the database change over time, and it's crazy error-prone to do it manually. Have you ever added a column to a dev environment just to forget to add it to production? This can easily take down your whole app. There must be a better way, right? Yes, thanks to "migration" tooling.

A migration is just a single file with sql queries to update a database schema and apply new changes to an existing database.

Another part challenge/frustration for developers is avoiding mistakes when accessing the database. Instead of manually updating every SQL query string that may reference the column you just changes, wouldn't it be nice if your tooling did that automatically? That's where Object-Relational-Mappers (ORMs) help. ORMs allow your code to access the database without writing SQL strings.

ORMs provide abstracted interfaces to the database, which allow you to describe the database schema in one place and have it apply globally.

|Object| A table in the database| |Relational| A relationship between tables (i.e. Users have Blog Posts))| |Mapping| Glue code that translates (aka maps) coding features to SQL query strings based on configuration (aka model)|

For example, this snippet models a user table, gets a user object from the database, update a column, then save it.

@Entity()
export default class UserEntity extends BaseEntity {
	@PrimaryColumn('varchar', {length: 30})
	id: string

	@Column('varchar', {unique: true, length: 30}) 
	email: string

	@Column('varchar', {length: 30}) 
	name: string
}

const user = await UserEntity.findOne({ where: { email: 'foo@bar.com' } })
user.name = 'Shirley'
await user.save()

That code actually works, too! It and this project use an ORM called TypeORM. Additionally, TypeORM provides type-safety and schema migration tooling (!!!).

Thanks to TypeORM's Typescript features, user.phone = '+1222222222' will throw an error, because there is no phoneNumber field in the model.

But whatabout migrations???

TypeORM has two workflows for database schema management: synchronize and migration.

|synchronize|TypeORM will immediately and greedily generate and apply SQL migrations on the fly. So if you changed the name of column in your code, TypeORM will drop the old column and create a new one, destroying any data that was in the old column.| |migration|You manage the migrations in code, and they are ran automatically.|

The synchronize mode is more convenient, but is obviously too dangerous for a published application. Therefore, this application uses synchronize for local development and migration for production. It's up to you as a developer to ensure that you create and test the needed migration files before you deploy(!). Luckily for you, TypeORM can to generate migration files for you, and these are usually good-enough.

To generate a migration file for what's currently in code vs. database:

npm run typeorm:migration-gen -- NameOfSnapshot

Deploying to AWS as Cloudformation Stack 🏁

Deploy assumes you have already

  1. Update the samconfig.toml file with your choices
  2. Run npm i - this installs the node dependencies
  3. Deploy to AWS using sam build && sam deploy
  4. View your stacks online at CloudFormation, or open your API at the URL from the deploy output.

To delete your stack, run:

aws cloudformation delete-stack --stack-name (name of your stack) --region us-east-1

If your stack is tied to CloudFront, you can bust CloudFront caches by:

aws cloudfront list-distributions | grep Id
aws cloudfront create-invalidation --distribution-id <id> --paths "/static/*"

Connecting to a custom domain

  1. If you haven't, create an ACM certificate for your domain. Even if your domain isn't in Route53, it's easy. You'll just need to add a DNS TXT record to your domain to confirm your ownership.
  2. Add your custom domain to API Gateway and connect it to your lambda
  3. Create a CloudFront app with your custom domain name that pulls from your API Gateway. CloudFront basically acts as a reverse proxy with caching.

Known Issues 🐞

When configured to sleep, API calls may timeout (28s) before the database finishes waking up. To tolerate this, the web application needs to retry API calls that fail due to database failures. Here is the expected 503 response from the API on failure:

{"message":"Service Unavailable"}

References and Special Thanks 😘