☠️ AWS-SERVERLESS-FULLSTACK ☠️
...is a template to create a low-management, autoscaling NodeJS api + database on AWS
What makes it cool:
- 100% serverless full-stack with Static frontend, NodeJS api and MySQL db
- Easy deploy management with AWS SAM + Cloudformation
- Run locally, AWS, or anywhere with generic file and db connectors
- All Javascript/Typescript
- Uses Fastify framework, which is mucho faster than Express
- DB ORM and schema management via Typeorm
- Type-strict
🌍
Table of Contents - Running locally
🏎 - API
☁ - DB Schema Management
🗂 - Deploying to AWS as Cloudformation Stack
🏁 - Known Issues
🐞 - References and Special Thanks
😘
🏎
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
- set up an AWS account
- have the latest version of the AWS CLI installed
- have the latest version of the SAM CLI installed
- Update the
samconfig.toml
file with your choices - Run
npm i
- this installs the node dependencies - Deploy to AWS using
sam build && sam deploy
- 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
- 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.
- Add your custom domain to API Gateway and connect it to your lambda
- 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"}