Environments with Express & Serverless
I recently worked on a Node Express API that tracks all holidays in the United States in 2019.
During development, I used a MongoDB Docker container and ran the Express API locally. For production, I imported all my data to a free Mongo Atlas instance, and deployed the API on an AWS Lambda function. I went with this approach for the obvious benefits that AWS Lambda provides – only running your code when a user visits your site. Goodbye 24/7 server fees! 💸
During the process of deploying my API to Lambda using the Serverless framework, I realized three things:
- I needed to load my environment variables in my lambda function 😨
- I didn’t want my “prod” lambda endpoint to end with
/dev/
(more on this later) 😕 - I needed to reference the lambda API Gateway URL from within my app 😬
As I worked through these, I realized that the common theme was simply managing multiple development environments! We can rewrite the above to better define our goals:
- Load app environment variables into lambda based on development environment
- Handle different environments in our Serverless config
- Export environment variables defined with Serverless back to app based on development environment
Starting to sound repetitive yet?
I solved this using the serverless-dotenv-plugin and this article. I’ll show you how I set up my serverless configuration with the plugin and some tips on what I learned along the way.
For demonstrative purposes, I’ve made a bare bones node express serverless app which you can refer to here. For the remainder of this article, I’ll be referencing this project.
The folder structure looks like this:
.
├── .gitignore
├── config
│ ├── .env
│ └── .env.production
├── handler.js
├── package-lock.json
├── package.json
├── serverless.yml
└── src
├── app.js
└── index.js
Disclaimer: The following is not meant to be a tutorial, just an overview and some commentary along the way! ✨
src
The src directory holds our application code.
Our express app is defined within app.js
, and we run it within index.js
. Some reasons for doing this is to have the app easily accessible by itself for testing and passing it to serverless to handle requests.
Another benefit is that our workflow sans serverless remains intact. We can still run package.json scripts like npm run dev
. In this example, I use env-cmd to handle environment variables and nodemon to automatically restart the process.
config
The config directory holds our .env files. In this example there’s two env files, .env
for development and .env.production
for production.
handler.js
The handler script imports our app from src/
and wraps it for serverless use within serverless.yml
.
serverless.yml
Finally, we can tie everything together in the serverless.yml
file to configure our lambda function.
Without loading our env files just yet, our serverless.yml
would look like this
But we need to manage our enviroments! To load our environment variables into our lambda function, we use the serverless-dotenv-plugin. In our serverless.yml
file, we define a custom section and add a variable called dotenv
to use the plugin.
This plugin supports a basePath option to point to a directory with multiple env files (our config directory). We access the variables we need for this configuration by ${env:SOME-VAR}
.
Quick note about stages
Stages in the Serverless framework allows us to create different environments for our app. If the stage is not defined in the serverless.yml
file, then Serverless will automatically default the stage to dev
.
Typically, we want to test our lambda independently of our production environment, hence the reason to dynamically set the stage based on our development environment. When we do this, the Serverless framework creates a different API Gateway project and we get a different endpoint host.
To test our lambda app locally, we can use the serverless-offline plugin. Make sure to update the plugins section in the serverless.yml
file.
Deploying
To deploy our app and use our production environment variables we run the serverless deploy command with NODE_ENV
. The serverless-dotenv-plugin will look for a file called .env.{ENV}
. If we don’t pass the NODE_ENV
argument, the plugin will look for a .env.development
file or default to .env
.
To deploy the production version of our app, we run NODE_ENV=production sls deploy
or sls deploy --env production
if the first doesn’t work.
To run the lambda offline, we run sls offine
. This will use our development environment variables.
Gateway URL inside app
What if you need the API Gateway URL within your application? For example, I wanted my holiday API to return an endpoint to view a holiday’s details for each holiday.
To achieve this, we need to reconstruct the API Gateway URL with a combination of Serverless variables and CloudFormation (read this article) so that we can use it as an environment variable in our app like process.env.GATEWAY_URL
. We will only need to modify the provider and custom sections of the serverless.yml
file.
So far, those sections look like this:
We define an environment variable GATEWAY_URL
in the provider section of our serverless.yml
file. This variable will depend on what stage we’re using. For production, we’ll construct the url. For development, we’ll use localhost:3000
. To do this elegantly, our provider GATEWAY_URL
will reference a variable of the same name in the custom section under a specific stage.
You might notice that the custom prod GATEWAY_URL
uses the stage and region values from the custom section itself.
Since we originally read those values from our env files with the dotenv plugin in the provider section, we can copy the references to the custom section whilst keeping the variables in the provider section. Then, in the provider we just reference their values from custom.
Now, we’re done! Here’s a look at the final serverless.yml
:
Now, you can access the API Gateway URL from within your app as an environment variable. Remember that when developing locally without serverless-offline, process.env.GATEWAY_URL
does not exist, so handle accordingly: