A list of programming principles I follow. A special thanks to my peers who have instilled many of these principles on me.
Organize models, services, controllers, etc. (ingredients) by component (recipe) then type (food group). Contributors (sous-chefs) can focus on a single component (recipe) without needing to jump around the entire project (kitchen).
app/
├─ component/
│ ├─ integration/
│ ├─ order/
│ │ ├─ create.js
│ │ ├─ delete.js
│ │ ├─ filter.js
│ │ ├─ route.js
│ │ ├─ model.js
│ ├─ payment/
│ ├─ shipping/
│ ├─ user/
├─ .gitignore
├─ package.json
├─ index.js
Code dealing with external integrations (database, APIs, AWS, etc.) can be sub-organized under a single component called integration
or external
. This makes it easy to find all the project integrations.
app/
├─ component/
│ ├─ integration/
│ │ ├─ aws/
│ │ │ ├─ sns/
│ │ │ │ ├─ publish.js
│ │ │ │ ├─ subscribe.js
│ │ │ ├─ sqs/
│ │ │ │ ├─ send.js
│ │ │ │ ├─ receive.js
│ │ ├─ mongodb/
│ │ ├─ stripe/
Helper functions for timestamps belong in a time component. Array helpers belong in an array component. The list goes on. Throwing so-called "util" functionality into a utils folder obscures the project structure and creates the perfect dumping ground.
Common gives zero context as to what common includes. Everything in common does something important; otherwise it would not be in the code base. Avoid common and instead place each “common” feature under an appropriately titled component.
If you end up creating a shared dependency across multiple projects, avoid using the word common in the title. Give the dependency a descriptive name.
Logs are useful when they aren’t polluted. Only log on errors.
Code should be written to succeed by default. If it's important enough to be logged during success, then it should persist in a database.
Avoid unnecessary logging libraries. There's no need for libraries that append timestamps, hostname, etc. to the output when most logging services (e.g. AWS Cloudwatch, Splunk, Datadog, logz.io, etc.) take care of this.
With excessive logging you also run the risk of logging secrets or other sensitive information.
Bonus: The less fluff you log, the longer the retention period you can afford.
Limit external dependencies to only the most critical, lightweight, and well-vetted options available. Be wary of nested dependencies; they introduce more attack vectors and increase time spent on vulnerability management.
In some cases, it's better to copy/paste snippets of code then to require an entire dependency.
// component/user/history.js
function lastPlayedSongByUser(user) { }
function lastPlayedBy(user) { } // better
let lastPlayedSong
let lastPlayed // better
The context (filename or folder name) should intuitively tells us that lastPlayed
is in regards to a song. No need to pollute the variable name any further.
function songs(playlist) {
// too generic; no intent revealed
}
function songsFromPlaylist(playlist) {
// playlist is redundant
}
function songsFrom(playlist) {
// just right
}
For functions that depend on integrations, prefix the function name with verbs like get
or set
:
function songBy(id) {
// not clear it calls an integration
}
function getSongBy(id) {
// more obvious it is calling an integration like DB or API
}
For all other functions, avoid prefixing:
function getYearFrom(date) {
// misleading; no integration is being used
}
function yearFrom(date) {
// better
}
If you need more, consider an options or config params; split on required vs optional.
function getSongsBy(artist, length, rating, page, offset) {
// too many params. requires length and rating to use paging.
}
function getSongsBy(artist, options) {
const [length, rating, page, offset] = options
// better. more options can be added without polluting params
}
Makes code easier to read.
if (action === 'INSERT') {
doSomething()
} else if (action === 'UPDATE') {
doSomething()
} else if (action === 'DELETE') {
doSomething()
} else {
throw new Error(`invalid action=${action}`)
}
switch(action) {
case 'INSERT':
doSomething()
break
case 'UPDATE':
doSomething()
break
case 'DELETE':
doSomething()
break
default:
throw new Error(`invalid action=${action}`)
}
Avoid referencing process or environment variables throughout the code. Create a config
module/package that takes care of validating, enforcing, and mapping all configs in a single location. Require the config package on code init to quickly error out when invalid configs are present.
Use both as needed to ensure folders and routes read like English. When in doubt, default to singular.
GET /shelter/2/animals Get all animals at shelter 2.
POST /shelter/2/animal Add an animal to shelter 2.
GET /shelter/2/animal/1 Get animal 1 from shelter 2.
DEL /shelter/2/animal/1 Delete animal 1 from shelter 2.
Avoid using global scope. Create a context module that can be passed around (injected) as needed. Connections, external SDKs, etc. should be included in context to make unit testing easy. Context should always be the first parameter in any function that requires it.
const ctx = {
db: new MongoDB(uri),
cache: new Redis(uri),
sns: new AWS.SNS()
}
init(ctx)
Inverted statements are harder to read.
if (!playlist.isPopulated()) {
// ok. isPopulated is also a bit obscure
}
if (playlist.isEmpty()) {
// better
}
Unnecessarily exposing functions and variables increases maintenance cost. It also runs the risk of being used in other components that it was not intended for.
They never get addressed. If it’s a critical todo, finish it right now. If not, document it in your project management tool.
Easier to read and looks better in more editors.
Code should read like English to most developers. Reserve the use of comments to cases in which context could be misinterpreted or assumptions were required.
External dependencies first. Internal dependencies next.
const assert = require('assert')
const aws = require('aws-sdk')
const mongodb = require('mongodb')
const createUser = require('../component/user/create')
const time = require('../component/time/format')
It can cause race conditions that are hard to track, reproduce, and fix. Consider letting cloud providers take on the burden by scaling horizontally instead.