How do I structure a monorepo serverless project with SLS?

March 2, 2020

In this article, we are going to talk about how to structure a monorepo serverless project with the Serverless Framework.

Ideal — Project Structure

Below you will see a high level view of what a serverless monorepo project may look like when working with the Serverless Framework.

lib/  # <-- shared service code
  response.js  # <-- adds CORS headers, success/failure methods
resources/  # <-- shared AWS resources
  api/  # <-- creates shell api, attaches auth
  db/ # <-- creates an aurora serverless database
  vpc/ # <-- creates an AWS VPC for private/public subnets
services/  # <-- logical groupings of functionality
  billing/  # <-- arbitrary service
    src/  # <-- organizes business logic for cloud function
      handlers/  # <-- lambda handlers
        invoices/
          index.js
        collections/
          index.js
    serverless.yml  # <-- reuses shell api

Let’s break down the top-level directories:

  • lib — shared service code
  • resources — shared AWS resources
  • services— logical groupings of functionality

Lib — shared service code

Inside of our lib directory we are going to have some shared code which each individual service will have the ability to pull in.

lib/  # <-- shared service code
  response.js  # <-- adds CORS headers, success/failure methods

In the example above we have a file called response.js . This file will handle the response methods for our AWS Lambda functions.

// lib/response.jsconst response = {};
let response = {};
response.success = ({ body }) => {
    return {
        body: JSON.stringify(body),
        statusCode: 200,
        headers: {
          "Access-Control-Allow-Origin": "*"  
        }
    }
};
response.failure = ({ error }) => {
    return {
        body: JSON.stringify(error),
        statusCode: 500,
        headers: {
          "Access-Control-Allow-Origin": "*"  
        }
    }
};
module.exports = response;

Above you can see above we are simply adding a body , statusCode , and headers to our AWS Lambda response. This will now lower future work for all developers writing AWS Lambda functions and prevent issues with CORS, which is one of the most common problems developers will initially hit when developing serverless REST APIs.

resources — shared AWS resources

Inside of our resources/ directory we are going to have additional serverless stacks which will be reused across all of our services.

resources/  # <-- shared AWS resources
  api/  # <-- creates shell api, attaches auth
  db/ # <-- creates an aurora serverless database
  vpc/ # <-- creates an AWS VPC for private/public subnets

If we expanded these subfolders we would see a structure like this.

resources/
  api/
    serverless.yml
  db/
    serverless.yml
  vpc/
    serverless.yml

Each one of these shared AWS resources stacks should also be outputting the necessary values to be consumed by the downstream services. An example, the api stack should output the REST API Id and the REST API Root Resource Id. Both are needed for services to reuse a singular AWS API Gateway REST API endpoint.

Here is what this example output section would look like if you were using Serverless Framework Pro (which we highly recommend at Serverless Guru)

org: serverlessguru  # <-- swap out for your org
app: my-app          # <-- swap out for your app
service: api
provider:
  ...
outputs:
  ApiId:
    Ref: ApiGatewayRestApi
  ApiResourceId:
    Fn::GetAtt:
    - ApiGatewayRestApi
    - RootResourceId

Now in my service I’ll be able to add this section which can make use of this section.

org: serverlessguru  # <-- swap out for your org
app: my-app          # <-- swap out for your app
service: ...
provider:
  ...
  apiGateway:
    restApiId: ${output:${self:custom.n}.ApiId}
    restApiRootResourceId: ${output:${self:custom.n}.ApiResourceId}
custom:
  n: my-app:${self:provider.stage}:${self:provider.region}:api

Note: I’ve made the variable custom.n named in this way to try and fit the code snippet in cleanly on Medium. These dynamic variable names can get quite long.

services — logical groupings of functionality

Inside our services/ directory is where the magic takes place or at least the majority of our application development.

services/  # <-- logical groupings of functionality
  billing/  # <-- arbitrary service
    src/  # <-- organizes business logic for cloud function
      handlers/  # <-- lambda handlers
        invoices/
          index.js
        collections/
          index.js
    serverless.yml  # <-- reuses shell api

Above you can see we have one service called billing . Inside this service we have two AWS Lambda functions that will be packaged individually invoices and collections .

Inside our serverless.yml file we can see how this might work.

org: serverlessguru  # <-- swap out for your org
app: my-app          # <-- swap out for your app
service: serviceA
provider:
  stage: ${opt:stage, "dev"}
  region: ${opt:region, "us-west-2"}
  apiGateway:
    restApiId: ${output:${self:custom.n}.ApiId}
    restApiRootResourceId: ${output:${self:custom.n}.ApiResourceId}
custom:
  n: my-app:${self:provider.stage}:${self:provider.region}:api
  base: ${self:service}-${self:provider.stage}
# Package the lambda functions individually by subfolder
package:
  individually: true
  exclude:
    - ./**
functions:
  invoices:
    name: ${self:custom.base}-invoices
    handler: index.handler
    events:
      - http:
          path: /invoices
          method: ANY
          cors: true
    package:
      include:
        - src/handlers/invoices/**
        - ../lib/**
  collections:
    name: ${self:custom.base}-collections
    handler: index.handler
    events:
      - http:
          path: /collections
          method: ANY
          cors: true
    package:
      include:
        - src/handlers/collections/**
        - ../lib/**

Above we are doing all the setup required to package our AWS Lambda functions individually. However, we are also doing the following:

  • Setting up CORS
  • Passing all paths/methods matching /collections or /invoices to the relevant AWS Lambda function while keeping some isolation
  • Using a consistent naming convention

Serverless Handbook
Access free book

The dream team

At Serverless Guru, we're a collective of proactive solution finders. We prioritize genuineness, forward-thinking vision, and above all, we commit to diligently serving our members each and every day.

See open positions

Looking for skilled architects & developers?

Join businesses around the globe that trust our services. Let's start your serverless journey. Get in touch today!
Ryan Jones
Founder
Book a meeting
arrow
Founder
Eduardo Marcos
Chief Technology Officer
Chief Technology Officer
Book a meeting
arrow

Join the Community

Gather, share, and learn about AWS and serverless with enthusiasts worldwide in our open and free community.