Every codebase is a living monster, your living monster 🧙
Clear conventions and best practices keep its ugly nature in check. Two things make the monster go mad:
- New contributors
- Leaving it alone for too long
I'm trying to revamp my own months-old PR. I left some TODO comments there. I have no idea what I meant and what I was supposed to do 😂
Over the years I developed some practices to tame this mess (mostly). Here are some ideas 👇
Types, don't even start without types
I don't understand how developers survive without types. I get that at times it makes you "faster", but most codebases are meant to live a long time. No-types is the first step into darkness.
I have a PHP background, the original and messy PHP (not sure how it's doing right now 🤔). My workflow was:
- Write code
- Test page locally
- Resolve all type issues (missing variables, unknown references, etc.)
- Reload page
- Check if logic is correct
- Repeat
Then I got burned by the "types" of Java (Android). I got the great idea of moving to JavaScript. It wasn't until TypeScript that I found my safe place again (together with functional languages like Haskell and OCaml).
Universal principle (always valid and undisputable): Types are a blessing 🪽
Don't add code "for later"
I found this especially true for frontend components. Sometimes you get the urge of adding just one more prop, in case you later need it for that new feature promised by the product team.
Bad. Don't.
(Another) Universal principle: it's impossible to predict how product requirements will evolve 🤦
I have seen codebases littered with props never used and features of unknown origin (and meaning). Keep the codebase to the exact minimum.
Unit tests for local development
Not all code can be saved with types (but probably a lot more than you think 👀).
For everything else try to keep it unit testable, locally.
Recent example: testing an API I implemented using HttpApi
from effect.
The API accesses a Paddle service (payments) and a postgres database. It's then used inside a frontend client to get products. Types cannot check the integration between all services, but tests can. Unit tests, local unit tests:
- Mocking of the Paddle SDK
- In-memory database using test containers
- Local node server to make client requests
Test your database using test containers 🏗️ 👉 Start container from image 👉 Connect to pg client 👉 Execute tests from container 👉 Close connection when done Working for every database 🪄
The tests make requests using the generated client to the local node server, which stores data on a test container database.
it.layer(LayerTest, { timeout: "30 seconds" })("MainApi", (it) => {
it.effect("api get product", () =>
Effect.gen(function* () {
const client = yield* HttpApiClient.make(MainApi);
const drizzle = yield* PgDrizzle.PgDrizzle;
yield* drizzle.execute(sql`
DO $$ BEGIN
CREATE TYPE "public"."currencyCode" AS ENUM('USD','EUR','GBP','JPY','AUD','CAD','CHF','HKD','SGD','SEK','ARS','BRL','CNY','COP','CZK','DKK','HUF','ILS','INR','KRW','MXN','NOK','NZD','PLN','RUB','THB','TRY','TWD','UAH', 'ZAR');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS "product" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"slug" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"description" varchar(255),
"imageUrl" varchar(255)
);
CREATE TABLE IF NOT EXISTS "price" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"productId" varchar(255),
"amount" varchar(255) NOT NULL,
"currencyCode" "currencyCode" NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "price" ADD CONSTRAINT "price_productId_product_id_fk" FOREIGN KEY ("productId") REFERENCES "public"."product"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
`);
yield* drizzle.insert(productTable).values({
slug: "test",
name: "Test",
description: "Test",
imageUrl: "https://example.com/image.png",
id: "test",
});
yield* drizzle.insert(priceTable).values({
productId: "test",
amount: "100",
currencyCode: "USD",
id: "test",
});
const { product } = yield* client.paddle.product({
path: { slug: "test" },
});
expect(product).toStrictEqual(
PaddleProduct.make({
id: EntityId.make("test"),
slug: "test",
name: "Test",
price: 100,
description: "Test",
imageUrl: "https://example.com/image.png",
})
);
})
);
});
If these tests pass, there is no doubt that the client will work (if CORS don't ruin your day 🙌).
Note: That's another reason why you should use Effect. With layers everything is easier 😎
Benefits of services and dependency injection with @EffectTS_ You can create testing layers and provide mock implementations just by composing services Includes configuration, env variables, database containers 🫡
Strive to implement lego blocks
Core principle for everything that scales: composability.
Everything grows from small and simple building blocks. Each block is easy to test and compose with other tested blocks. The final result is flexible code that builds on a solid foundation.
This principle is everywhere: components, styles, plugin systems, bundlers, APIs, and more.
The code mentioned today is part of my upcoming Paddle Billing Payments Full Stack TypeScript App.
Meanwhile, new update to my Effect course, adding the new Effect.Service
API.
A lot is moving 🚀
See you next 👋