Skip to content
On this page

Matthew's Marvellous Coding Manifesto

This is an incredibly opinionated "coding guide"/style guide for my personal reference. It's a collection of things I've learned over the years that I think are important to keep in mind when writing code.

Keep the following in mind when reading:

  • It's based on my own experience (it's anecdotal).
  • It's based on what other people have said and I believe to also be true (probably also anecdotal).
  • Sometimes it doesn't always work out, so try something different.
    • You're not supposed to follow this guide 100%.
    • If you disagree with something, think about why you disagree with it, and understand why your opinion makes more sense in your scenario.

This is updated whenever I find something interesting and have the will to write about it.

You spend as much time reading code as writing code

Write your code for other people, and for future you to read.

  • Always comment your code while you're writing it. Sometimes do it before even writing code to block out your thoughts.
    • Comments should describe what the purpose of code is, not just what it does.
    • At the start of functions, write a full description using code comments. Be descriptive but concise.
    • Imagine you're writing for a junior dev to understand your code. Include plain English to explain what the function does, then go into technical details if necessary.
    • Don't write useless comments to make yourself feel good.
  • Avoid abbreviating variable, class or method names. amount is better than x, and numberOfUsers is better than nusr.
    • Variable name length isn't limited in most languages.

Prioritise working code over beautiful code

  • You shouldn't be afraid to throw away code.
  • Code is often rewritten or completely discarded based on business requirements.
  • That doesn't mean bad code it means it doesn't need to be perfect.
  • Be prepared to hate your own code you wrote in a few months. "Who wrote this garbage?" - it was past me.

Immutability rocks

  • Avoid modifying parameters in a function, instead return a new object
    • Don't write a function that messes with whatever you pass to it
    • Other people will get very upset if you do
  • Don't go overboard with things like reducers
  • This usually makes code way easier to debug and understand
  • BUT don't go overboard. It's fine to push to an array where it makes sense

Bad:

ts
function getUserFullName(user: User) {
	user.fullName = `${user.firstName} ${user.lastName}`;
}

getUserFullName(user);

Good:

ts
function getUserFullName(user: User) {
	return `${user.firstName} ${user.lastName}`;
}

const newDetails = {
	...user,
	fullName: getUserFullName(user),
};

Be a Never Nester

Avoid nesting code more than 3 indentations.

  • Use inversion - exit functions early.
    • Validate first, then go down the happy path.
  • Use extraction - move parts of code out into their own functions.
    • Avoid when it makes code harder to understand.
    • Prioritise co-location of related code over function length.

Bad:

ts
async function addUser(details: UserDetails) {
	if (user.name !== null) {
		if (user.age >= 18) {
			await db.add(details);
		} else {
			throw new BadException('User must be 18 or older');
		}
	} else {
		throw new BadException('User name must be specified'); // this is related to the code allll the way at the top
	}
}

Good:

ts
async function addUser(details: UserDetails) {
	if (user.name === null) {
		throw new BadException('User name must be specified');
	}
	if (user.age < 18) {
		throw new BadException('User must be 18 or older');
	}
	
	await db.add(details);
}

This splits the code into two "sections" where validation happens first, and errors are handled, then the happy success case is always at the end of the function.

Code co-location

  • Avoid jumping around different files or functions to understand a process.
  • Keep related code together.
    • Don't worry too much about function length.
    • Separate code out into functions where it's reused.
  • Folder structure should reflect concepts rather than code patterns.
    • Bad:
      • controllers/
        • user.controller.ts
        • post.controller.ts
      • services/
        • user.service.ts
        • post.service.ts
    • Better:
      • users/
        • user.service.ts
        • user.controller.ts
      • posts/
        • post.service.ts
        • post.controller.ts

Code is cheap, perfect code is expensive

  • You will never achieve perfect code. Trying to do so wastes time.
  • Always write code that is good enough.
    • Write code that is easy to refactor later.
  • Half the time you end up throwing away your code because business decisions change.
  • Reduce complexity.
    • Start out by writing all your code in one place.
    • Break it apart later when you need to.

Abstraction is a trade-off

Abstraction isn't just to get rid of repeating code. Repeating code isn't bad. The more you abstract your code away, the more coupling you get.

  • Use abstraction when a concept makes sense to abstract. E.g. An Object class in a game, that has an x, y and z coordinate. Every other thing in the game's 3D world is also an Object.
  • Use abstraction when something needs to be generic - when you can pass multiple kinds of a class to a single method.
    • 9/10 times you can just use an interface here rather than an abstract class.
  • Use composition over abstraction (where possible). Include a class instance to do something rather than extend that class.
  • Abstract classes are difficult to refactor. The parent class's implementation affects every descendant class.

Use linters

Use ESLint. Yes it's annoying but it's better than finding bugs in prod.

  • Use in conjunction with Prettier to automatically format code on save.
  • Don't just ignore errors, use them to fix your code.
Maybe controversial?

Linters shouldn't prevent you from building code. Never slow down your developers.

Prefix UUIDs

Use UUIDs for entity IDs for security.

  • You can generate the ID before the entity is stored
  • It's incredibly unlikely someone will ever guess it, making it more secure (IDOR Insecure Direct Object Reference)

Use entity names in the UUID to make life easier

Idea borrowed from Stripe, use UUID v4, but stick the entity name in front of it. E.g. instead of 2D01C1D7-6535-4B3B-80A2-9B1EC27E77D4 make it user_2D01C1D7-6535-4B3B-80A2-9B1EC27E77D4.

  • Makes identifying data in logs so much easier.
  • Also a great sanity check when looking at database records.

Use simple, predictable libraries

We tried MikroORM which boasts far better features than Prisma, like automated rollback on errors. The problem is that it starts doing things you don't expect, like rolling back queries in a stream even though you've caught the error.

The problem is there was less control, and it did things behind the scenes which wasn't immediately obvious by reading code.

  • Choose a library that is well-maintained and has a great community.
  • Use tried and tested tech, unless you're using something that will improve DX.
  • Usually, the simpler the better. The less you have to learn to adopt it the better.

"OOP sucks" by Brian Will

Watch presentations by Brian Will:

Very compelling arguments.

Problems with OOP:

  1. It conflates data with methods on that data by encapsulating both into a single object. Data is data. Methods that act on that data should accept the data as a parameter and spit something out (a more functional approach).
  2. It becomes a philosophical exercise about what an object truly is. Writing things as objects when in fact they are concepts or procedures.
  3. Single responsibility principle rarely pans out. For this to really work you need ways to pass data between different instances without the instances directly referencing each other, or else you are violating that principle. End up with a giant spaghetti mess that's hard to debug.
  4. Breaking code into different parts makes each individual part easier to understand but makes the whole more difficult. Usually it's more important to understand what the entire process is rather than understand each individual part. It's not fun when a simple process requires going through 50 class factories.
  5. Design patterns encourage complex solutions to simple problems.
  6. Inheritance is dangerous. Encourages abstraction.
  7. Polymorphism is dangerous. Encourages more abstraction.
  8. It's pretty easy to end up with bad abstractions.
  9. Encapsulation restricts development speed and ends up leaking either way.
  10. UML diagrams are very useful but following UML precisely is a waste of time and makes thinking about problems harder. You end up with a giant mess of connections between boxes.

What is the solution?

  1. Procedural is great. It is easy to understand and makes sense. Pass data to functions.
  2. Classes are fine. Kinda works like a namespace. Problem is now you need to instantiate objects just to use methods. May as well just define functions inside a file.

What I like about OOP:

  1. Editor completion - "string".slice(0, 1) vs slice("string", 0, 1).
  2. It can help group related functions and data together to avoid confusion.
  3. You can use the good parts of OOP with the good parts of procedural programming and functional programming.

MVC and code repetition

Repeating code isn't the end of the world. DRY is only really important when it's a complex process, or you're copy-pasting thousands of lines of code.

  • Calling the ORM directly from a controller rather than a service is fine. You can pick and choose exactly what fields you need.
    • "But what if we change the ORM?" - it's very unlikely. But either way you'll still have to refactor just about every place the ORM is used. If you're using the same complex query in multiple places maybe you should then create a function for it.
  • The service should contain functions that help you transform/aggregate the data, or do something useful with it.

Services should be thought of as "groups of functions". Good examples of services are things that will be used throughout the application by unknown actors.

  • Send emails
  • CRON scheduler
  • Storage access
  • Encryption
  • JWT generation and validation
  • HMAC
  • Compression

These are basically internal libraries. They shouldn't handle any business logic.

You can also create services that do handle business logic. Rather than writing a service that's essentially just a wrapper around your ORM it should provide useful, reusable methods. E.g. Fetch and calculate the total number of likes on all of a user's posts, or find all of the users within a few km of you. These methods are not just "fetch a user's posts" because that is something you should do with the ORM.

  • Your service should not just wrap the ORM. It's usually just monkey patching.
  • Service methods should be reusable and provide useful reliable business logic handlers.

This might seem a bit opposite to the idea of MVC, where the controller should handle business logic and the services don't. But in reality some services are going to end up handling business logic and some aren't. When looking at backend code especially, the controller is effectively the view and the controller.