Monorep Structure and Best Practices


#1

I thought I would start a dedicated thread for Monorepos. This is something @jayair committed to writing a few chapters about shortly.

In the mean time the following resources were shared by @jayair

Cross Stack References
Shared API Gateway Resources

Even with these resources shared, this is something I have been struggling with. So I wanted to share what I was planning and see if anyone else has info that can help. Maybe together we can compile a meaningful list.

My Current Folder Structure

package.json
webpack.config.js
.babelrc
jest.config.js
resources/
  dynamo-table.yml
  s3-bucket.yml
  package.json
  serverless.yml
services/
  users/
    create.js
    create.test.js
    list.js
    list.test.js
    delete.js
    delete.test.js
    update.js
    update.test.js
    get.js
    get.test.js
    package.json
    serverless.yml
  projects/
    create.js
    create.test.js
    list.js
    list.test.js
    delete.js
    delete.test.js
    update.js
    update.test.js
    get.js
    get.test.js
    package.json
    serverless.yml
  etc/
    ...

Example Service Yaml File

service: service-name

plugins:
  - serverless-webpack
  - serverless-dynamodb-local
  - serverless-offline
  - serverless-domain-manager
  - serverless-plugin-stage-variables

custom:
  webpack:
    webpackConfig: ../../webpack.config.js
    includeModules:
      forceExclude:
        - aws-sdk
  stage: ${opt:stage, self:provider.stage}
  stageVariables:
    env: ${self:custom.stage}
  tables:
    projects: projects-${self:custom.stage}
  domains:
    prod: api.example.com
    stage: stage-api.example.com
    dev: dev-api.example.com
  customDomain:
    domainName: ${self:custom.domains.${self:custom.stage}}
    basePath: 'projects'
    stage: ${self:custom.stage}
    createRoute53Record: true
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migrate: true
    migration:
      dir: migrations

package:
  exclude:
  - coverage/**
  - migrations/**
  - .circleci/**
  - .git/**
  - tests/**

provider:
  name: aws
  runtime: nodejs8.10
  stage: dev
  region: us-east-1
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:*:*:table/${self:custom.tables.projects}"

functions:
  create:
    handler: create.default
    events:
      - http:
          path: /
          method: post
          cors: true
    environment:
      PROJECTS_TABLE: ${self:custom.tables.projects}

  list:
    handler: list.default
    events:
      - http:
          path: /
          method: get
          cors: true
    environment:
      PROJECTS_TABLE: ${self:custom.tables.projects}

  get:
    handler: get.default
    events:
      - http:
          path: /{id}
          method: get
          cors: true
    environment:
      PROJECTS_TABLE: ${self:custom.tables.projects}

  update:
    handler: update.default
    events:
      - http:
          path: /{id}
          method: put
          cors: true
    environment:
      PROJECTS_TABLE: ${self:custom.tables.projects}

  delete:
    handler: delete.default
    events:
      - http:
          path: /{id}
          method: delete
          cors: true
    environment:
      PROJECTS_TABLE: ${self:custom.tables.projects}

Additional Questions

  1. Do I need all those plugins? I want to use es6 syntax and have been using webpack prior to moving to a monorepo and am trying to figure out if I need everything in the servless.yml file for each service.
  2. Do i need the domain specified in each service? All are part of api.example.com or the other stages.
  3. Currently I set the base path different for each service, but ideally I would be using v1/ and in the future v2/ for versioning. What is the recommended way to handle this?
  4. Dynamo DB Local will only run one service, which makes sense. Is this the best way to test locally?
  5. I have a single endpoint that can interact with two databases (not shown here). Does it make sense to do this, or have that service call “update” on the real update endpoint. Seemed weird to have lambda call a api that is a sister service.
  6. Is this an ideal setup. Can I contain webpack in root?
  7. Does the package.json files need all the babel and webpack installations to run the serverless-webpack plugin?
  8. Does it make sense to keep seperate package.json file with serverless-webpack, since treeshaking should reduce the bundle size. Do people typically use learna or yarn workspaces, or just roll there own, or keep one package.json?
  9. Does keep resources in a separate deploy cause problems? Does this make sense? I see the “shared API gateway” resource, and am curious if this concept exists for s3 buckets, dynamo db, cognito identity or user pools.

#2

I’ll touch on a couple of things quickly.

Keeping resources separate is completely fine. You’ll just use cross-stack references in the other services that are referencing it. It’s also quite likely that you won’t be deploying the services with the resources as often.

For webpack, AFAIK it wouldn’t help running it as a whole on the entire app since each service gets packaged on its own. So effectively you are treating each service fairly independently. And you are going into each service to do a serverless deploy.


#3

That is correct, sls deploy will happen in each service.

I think serverless-webpack has a issue with monorepos, where using a shared config does not understand the context of where sls deploy is running from.

See here - https://github.com/serverless-heaven/serverless-webpack/issues/430


#4

Ah yeah I recall running into that. So internally we’ve been it doing it for each service.


#5

Makes sense, that was my next attempt. I was just hoping to no have to duplicate so much stuff. Thank you for being so responsive @jayair.


#6

@khrome83 We just published a new section on this. And it comes with a new sample repo to test with.