I had a SPA (single page application) written in React communicating to a REST JSON API written in nodejs and hosted on Heroku as a monolith.
I migrated to AWS Lambda and split the monolith into 3+ AWS Lambdas micro services inside of a monorepo
The following project structure is good if your SPA requires users to login to be able to do anything.
I used a single git repository where I have a folder for each service:
Inside of each of the services' folders I have a serverless.yml defining the deployment to a separate AWS Lambda.
Each service maps to only 1 function index
which accepts all HTTP endpoints. I do use 2 environments staging
and production
.
My AWS Lambdas are named like this:
- example-api-staging-index
- example-api-production-index
- example-app-staging-index
- example-app-production-index
- example-www-staging-index
- example-www-production-index
I use AWS Api Gateway's Custom Domain Names to map each lambda to a public domain:
You can define the domain mapping using serverless.yml Resources or a plugin but you have to do this only once so I did manually from the AWS website console.
My .com domain was hosted on GoDaddy but I migrated it to AWS Route 53 since HTTPS certificates are free.
app service
- /bundles-production
- /bundles-staging
- src (React/Angular single page application here)
- handler.js
- package.json
- serverless.yml
The app service contains a folder /src with the single page application code. The SPA is built locally on my computer in either ./bundles-production or ./bundles-staging based on the environment. The built generates the .js and .css bundles and also the index.html. The content of the folder is deployed to a S3 bucket using the serverless-s3-deploy plugin when I run serverless deploy -v -s production
.
I defined only 1 function getting called for all endpoints in the serverless.yml (I use JSON instead of YAML):
...
"functions": {
"index": {
"handler": "handler.index",
"events": [
{ "http": "GET /" },
{ "http": "GET /{proxy+}"}
]
},
},
The handler.js file returns the index.html
defined in /bundles-staging or /bundles-production
I use webpack to build the SPA since it integrates very well with serverless with the serverless-webpack plugin.
api service
I used aws-serverless-express to define all the REST JSON API endpoints. aws-serverless-express is like normal express but you can't do some things like express.static()
and fs.sendFile()
.
I tried initially to use a separate AWS Lambda function instead of aws-serverless-express for each endpoint but I quickly hit the CloudFormation mappings limit.
www service
If most of the functionality of your single page application require a login it's better to place the SPA on a separate domain and use the www domain for server-side rendered landing pages optimized for SEO.
BONUS:
graphql service
Using a micro service architecture makes it easy to experiment. I'm currently rewriting the REST JSON API in graphql using apollo-server-lambda