Laying out a repository for serverless projects
This is Cicada Note #5 . For a background on Cicada and Cicada Notes, read Introducing Cicada.
One of the first things I learned when I started coding with Javascript is that there are very few idiomatic, or “community standard”, ways of doing things. An example is how to start with where to put files in a source tree.
Even when the specific context gets narrower - a Node project; or a Node project to develop Lambda applications - there’s still little agreement from different groups of people. For example, if you wanted to start work on a Lambda TypeScript application today you could get three different repository layouts from three different tools, all of them associated with AWS (sam init
, cdk init
, projen).
Because there are no agreed community standards I feel entirely free to blaze my own trail! And so in this article I start to explain how I layout a code repository. This is for Cicada, which is a serverless TypeScript app using CDK as a deployment tool, however the ideas here are largely the same as I’ve used for over 20 years for different languages and for different target platforms. This article explains what files and directories I put in the project root, subsequent articles will explain the locations deeper within the source tree.
What are we trying to solve here?
Cicada is an application that is deployed all-at-once. Cicada’s source code repository is therefore not a “monorepo”, by my definition of a monorepo anyway, in that it isn’t one repository that contains code for multiple applications.
Therefore what I want from my source repository is a place to store source code, tests, tools, documentation, automation scripting, and configuration, all versioned together, for one deployable application. Some of my practices would change for a monorepo.
The project root
The root directory of the repository is also, semantically, the project root - it’s the place where all files in the repository are based from AND it’s the place where I run manual tasks.
The project root contains a number of files and directories. I’ll talk about files first.
Project root files
In recent years Javascript projects have become infamous for having a huge number of files stored in the project root. Mostly these are configuration files for the project, or tools used by the project. I hate this state of affairs, and if I have any control of things I do things differently in projects I work on.
Why do I care so strongly about this? Having a large number of files in the project root has the following problems:
- It’s “information overload” for people coming to the repository for the first time - where should they get started?
- Different files can exist for very different reasons. Usually we scope the meaning of a file by putting it in a subdirectory. The more files we have in the root, the fewer we have that have their meaning scoped by directory.
- …. and it’s just untidy! I might be weird (OK, I know I’m weird) but I like to work in a tidy place, and if the root of my repository is an unsightly mess then what hope is there for the rest of the repository?!
My “rule of thumb” is that files should only exist in the root if they need to, because of tools, or because it makes life easier to do so. If a file doesn’t need to exist in the project root for either of these two reasons it should go in a subdirectory.
An example of the first of these reasons is the package.json file - the top-level configuration file for most Node applications. My IDE and command-line tooling assume that they will find the package.json in the project root, and so that’s where it goes in my tree.
An example of putting something in the root because it makes life easier is my deployment helper script - deploy.sh . I could put this in a subdirectory, but it’s quicker to type ./deploy.sh
than to type ./src/tools/deploy.sh
.
With these two rules in place I should be able to pick any file in the project root and know why it’s there.
Let’s look at Cicada’s root directory at the current time of writing.
Cicada's root directory files, stored in source control
Files that have to be in the root because of tools
The following need to be in the root because of either command line tools, or because (to the best of my knowledge) my IDE of choice (Webstorm) expects them to be there:
Files stored in source control
- .eslintrc.js
- .gitignore
- .nvmrc
- .prettierrc.json
- package-lock.json
- package.json
- tsconfig.json
Ephemeral / user files
- .env
I think it’s pretty likely that .gitignore, .nvmrc, and the package.json files are always going to need to be in the root. I’m fine with .env existing in the root since it’s a single, cross-cutting, file. I’d ideally move the other files (related to ESLint, Prettier, TypeScript) to a subdirectory, but I haven’t figured out a way to do that and have everything play nicely together.
An example of a file which is not in the project root, even though many examples would include it there, is cdk.json, which instead exists under /src/cdk . It turns out that the CDK tooling is quite happy for it to live in a subdirectory.
Files that are in the root because it makes life easier
deploy.sh exists in the root because I use it frequently from the command line. If I used a universal “go script” for performing standard development-time tasks I would put that in the root instead.
README.md, LICENSE, and ContributorAgreement.txt are documentation files that I want to stand out (and README.md and LICENSE by an actually agreed-on convention should exist in the root).
Finally there’s .env-template . It just about survives in my project root because I want it to stand out, and because it’s where any actual .env files live, based on this template.
Project Root Directories
The Javascript ecosystem’s approach to root files annoys me, because it’s messy. But it gets worse - and that’s the ecosystem’s approach to directories. There is almost no common standard - even just for application source code. Should application code exist in the root, in /lib, /src, /main, or in one of several subdirectories of the root? And then there’s static content (which is often treated differently), test code, tooling code, configuration, etc.
Because of this entire lack of common ground I instead rely on how I worked with Java and .NET projects over 20 years ago, as follows:
Cicada's first level subdirectories, stored in source control
- All application source - whether code or configuration - for runtime code, deployment scripting, and tools, lives somewhere under /src
- All tests, and code used only by tests, lives somewhere under /test
- Documentation lives in /docs
I describe how I organize source code within /src in this article
Tool-specific directories may be checked in at the project root if required or if it makes sense:
- /.github contains GitHub specific configuration, e.g. Github Actions Workflow definitions, because GitHub needs it there
- I have a /.sharedIdea directory for IDE (webstorm) shared configuration files which have repository-wide scope
Ephemeral directories in the project root are limited to the following:
- All ephemeral content related to build or deployment lives under /build
- Any “standard” tooling or operating system subdirectories are left as they are with their defaults (e.g. .git, .idea, node_modules, .DS_Store)
This strategy allows me to keep the project root organized, and gives me a very clear first decision point of where in the tree I’m going to put a new file.
Another example of how I differ from CDK’s standard-ish layout is that the cdk.out directory lives underneath /build. This helps keep the project root a little more clean. Changing the configuration for CDK for this is pretty simple.
Summing up
In this article I started to explain how I layout a code repository, and the reasons why. This article looked just at the project root directory, and I dig into the /src/ directory specifically in the next article in the series.
Feedback
I enjoy hearing what people think about how I’ve built Cicada, and also about my writing in Cicada Notes. If you have feedback either email me at mike@symphonia.io, or contact me at @mikebroberts@hachyderm.io on Mastodon, or at @mikebroberts.com on BlueSky.