Black Sheep Code
rss_feed

If I were building a start up from scratch, these are the things I would include (or have a plan to include).

Published:
The term 'green field' in software development refers to building a project from scratch

The problem any codebase can face is that they make really good headway early on, and then after a year or two everything runs into the mud, and that's when greater effort on adding said tests or tooling.

Particularly as it relates to testing, it is much easier to include some tests when you're writing something new, than it is to add tests later on.

But assuming this is a start up context, or there's otherwise some kind of deadline (perhaps this is a simple proof of concept that we need to whip up and not spend a year building), we can't spend all our time creating a gold-plated application that does very little.

I would argue that there's a middle ground - some toolings we can add right from the start, they're likely to be saving us time. Think - TypeScript, OpenAPI specs, build pipelines. Other toolings are an eventual must-have, and we'll build our application with those in mind - 'when we come to adding tooling X, are we making it easy for ourselves, and what things do we want it to do?'.

Immediate must haves

OpenAPI specs

OpenAPI has a wealth of associated tooling, code generators, documentation generators and the like. If we take a 'spec first' strategy to building our application this is likely going to save us a lot of time building our first MVP.

Storybook and Storybook Testing

I don't see creating Storybook stories as 'extra work'. They're actually a faster way of working as components can be created in isolation, without having to navigate to them with the application dev server.

Comprehensive tests can be extra work - but my philosophy here is 'at least write one test' - that is, if we can show one test working for a given component, then later it is going to be a lot easier to write more. Often the difficulty of writing tests isn't the actual business logic being tested - but the scaffolding the dependencies to get the unit of code into a runnable state.

Documentation generators

A documentation generator like API Extractor is easy enough to set up, and will save a lot of time of manually writing documentation. The idea is to as much as possible, write our documentation/usage guide in code (via TSDoc/JSDoc), this way we both get our documentation for free, as well as keep our code comments up to date.

Static checks

TypeScript is a must have, and that's pretty uncontroversial these days.

I would use an opinionated formatter like Prettier - no arguing of code styles.

I would have ESLint (or possibly Biome?) set up with a minimal rule set. Have ESLint to fail on yellow warnings.

The idea with a lint strategy is to build up rules as you discover the need for them, rather that to come out of the gate with a bunch of rules that aren't necessarily helping you.

Build pipelines

A minimal build pipeline should include:

Tools I would avoid

GraphQL

Unless the application inherently contains graph heavy data models, like some kind of relationship explorer, the drama and caveats that come with GraphQL means that developing with it will just add a lot of friction to our application.

Document Databases

As I understand it, document databases are well suited for high volume transaction type data, but given that this is proof of concept, we're better off sticking to tried and true SQL.

Easy enough to adopt, so lets do it

Test suite speed reporting

A problem that can sneak up on us is our tests initially run fast and give us a lot of value, they're catching errors before we merge our PRs etc. But over time they become slower, and slower, and sloooower. Eventually our test suites are causing us friction, we're unable to merge our PRs in a timely manner.

If each PR can report on how long the test suites are taking, we can predict how long they'll be taking in a year or so, and make efforts to parallelise them, or identify smells that are making them take too long. (For example - be aware that React Testing Library's getByRole can be very slow. See: testing-library/react-testing-library#1213 Slow getByRole leads to test timeouts)

Bundle size metric reporting

Similarly to test speed, our bundle size can balloon without if we don't pay attention. There are CI tools like this one that can report on bundle size for each PR.

OpenAPI generated client SDKs

If we have an OpenAPI spec then client SDKs are very easy to generate for a range of languages using OpenAPI generators.

It might be the case that some of these client SDKs prove not to be fit for purpose. In that case - it would be good to know about that as early as possible.

Pretty high up on the 'if this is getting serious we should adopt these' list

Contract Tests

We already have an OpenAPI spec, so at this point let's make use of it and make sure our API is behaving as specced. You can see my post here about writing contract tests from OpenAPI specs.

Load testing

If the product is looking like it has some merit and is gaining traction, we need to be aware of how much headroom we have in terms of our current performance bottlenecks.

We can use a tool like k6.io to conduct load tests on our application. The point here isn't that we need to start optimising our application, what we need to know is when do we need to start loading testing our application.

Release Management

I would use a tool like changesets for release management.

We want a formalised release process that:

  1. Avoids us making breaking changes as much as possible. (ie. follow a deprecation and clean up strategy)
  2. Defines what a breaking change is. Is changing a class name on an HTML element a breaking change?
  3. Gives a process for making breaking changes/major version bumps that isn't immediately disruptive.

Observability

My experience has been that observability has been a 'thrown over the fence' type type concern, either rightwards to SRE/Infrastructure types (eg. performance and load monitoring) or from the left from product owner types (eg. Real User Monitoring).

If we can instil the observability tooling as a core part of developer's technical toolkit, then developers will have more ownership of the metrics and make it work for them. For example, say a developer notices an minor algorithm that looks inefficient. The developer could:

  1. Spend time optimising it, without knowing what the value of the optimisation is.
  2. Wait for someone else in the business to complain about it and then fix it.
  3. Put in an observability experiment to determine how often this algorithm is being encountered and what its impact is.

Tooling I would add as/before we're hiring more people

Expanding team size can be risky, albeit necessary, manoeuvrer - a dilution of technical culture can occur, where a previously cohesive technical direction is subjected to people's different experiences and understandings.

At this stage of a codebase's lifecycle the founders need to let go of the reins a little and allow the codebase to take on a life its own.

In order to make sure we're not ruining what was hopefully a really nice codebase to this point, I would add the following tooling:

Codeowners configuration

A CODEOWNERS file will allow us to establish clear code ownership boundaries.

I'm a fan of clear code ownership - in order to avoid diffusion of responsibility ('responsibility shared is no responsibility at all'). Clear code ownership puts the right incentives on developers to maintain a tidy codebase.

We're building a quality product

Release Artifacts - Docker Images

The idea here is, if our product provides programmatic access via a REST API, then let's make it easy for our consumers to integrate our application in a test environment as well. We provide them a Docker image that have our application providing test data for various scenarios.

Release Artifacts - Frontend test helper functions

If our application contains frontend components, then it can be helpful to provide a series of helper functions to will assist in interacting with the the components in test contexts.

For example, say we our product provides some kind of credit card payment form that others will integrate in our website.

Rather than having our users write their tests like:

// Cypress tests 
cy.findByRole("textbox", {name: "credit-card-number"}).type("5555555555554444");
cy.findByRole("textbox", {name: "credit-card-name"}).type("Test User");
// etc

We might want to expose a function like such:

cy.fillCreditCardForm({
    number: "5555555555554444", 
    name: "Test User", 
    expiry: "01/26", 
    cvc: "123"
})

Very convenient.

Have I missed anything?

Let me know if you think I'm either overcooking it, or there is something blindingly obvious that should be included in some new codebase.



Questions? Comments? Criticisms? Get in the comments! 👇

Spotted an error? Edit this page with Github