Studying source code can undoubtedly change the trajectory of your dev career. Even looking beneath the surface just one level can set you apart from most average developers.
It's the first step to mastery!
Here’s a personal story: In my current gig at an AI/ML startup, the team couldn't figure out how to get Neo4j data from the server to the frontend for a visualization, and they had a presentation in 12 hours. I was brought in as a freelancer, and you could clearly see the panic. The problem was that the data returned by Neo4j was not in the correct format expected by the visualization tool, neo4jd3.
Imagine this: Neo4jd3 expects a triangle, and Neo4j returns a square. That’s an incompatible mismatch right there!
We might do graph data science with JavaScript and Neo4j in Mastered soon! This image is nostalgic.
There were only two choices: redo the entire Neo4j backend or study Neo4jd3's source code, figure out the expected format, and then create an adapter to transform the square into a triangle.
neo4jd3 <- adapter <- Neo4j
My brain defaulted to reading the source code, and I created an adapter: neo4jd3-ts.
import createNeoChart, { NeoDatatoChartData } from "neo4jd3-ts";
The adapter is NeoDatatoChartData
, and everything else is history. I took this lesson to heart, and every chance I get, I go a level lower in every tool I use. It has become so prevalent that sometimes I don't even read the documentation.
This approach changed my career tremendously. Everything I do looks like magic. In a few months, I was leading critical server migrations and projects, all because I took a step towards the source.
This is what this series is about: not settling for the API, but going beyond, learning to recreate these tools. Breaking out of being average in this world of AI hype is what makes a developer valuable beyond average!
My plan with this series is to study popular JavaScript libraries and tools, figuring out together how they work and what patterns we can learn from them, one tool at a time.
Since I am mostly a backend engineer (full stack, yes, but handling the backend 90% of the time), there's no better tool to start with than Express.js.
My assumption is that you have programming experience and a good grasp of the fundamentals of programming! You might be classified as an advanced beginner.
It'll be really hard and tedious to try and learn/teach source code while teaching fundamentals. You can join the series, but expect it to be hard. I cannot cover everything, but I will try as much as I can.
This article is pre-Express for a reason: I decided to cover a very small library Express depends on, merge-descriptors, which, as I write this, has 27,181,495 downloads and a mere 26 lines of code.
This will give us an opportunity to establish a structure and allow me to introduce object fundamentals which are crucial in building JavaScript modules.
Before we proceed, make sure you have the Express source code and merge-descriptors in your system. This way, you can open it in an IDE and I can guide you with line numbers on where we are looking.
Express is a beefy library. We'll cover as much as we can over a few articles before we move on to another tool.
Open your Express source in your IDE, preferably with line numbers, navigate to the lib
folder, and open the express.js
file, the entry file.
On line 17, here is our first library:
var mixin = require('merge-descriptors');
Usage is in lines 42 and 43:
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
Before we even explore what is happening here, we need to take a step back and talk about objects in JavaScript, beyond the data structure. We'll discuss composition, inheritance, prototypes, and mixins—the title of this article.
Close the Express source code, and create a new folder somewhere to follow along as we learn these crucial object fundamentals.
An object is an encapsulation of data and behavior, at the core of Object-Oriented Programming (OOP). Fun fact: almost everything in JavaScript is an object.
const person = {
// data
name: "Jane",
age: 0,
// behavior
grow(){
this.age += 1;
}
};
Everything between the opening and closing braces in the person
object is called object own properties. This is important.
Own properties are those directly on the object. name
, age
, and grow
are person
's own properties.
This is important because every JavaScript object has a prototype
property. Let's encode the above object into a function blueprint to allow us to dynamically create person
objects.
function createNewPerson(name, age){
this.name = name;
this.age = age;
}
createNewPerson.prototype.print = function(){
console.log(`${this.name} is ${this.age}`);
};
const john = new createNewPerson("John", 32);
The prototype is how JavaScript objects inherit properties and methods from other objects. The difference between Own Properties
and Prototype
is when accessing a property on an object:
john.name; // access
JavaScript will first look in Own Properties
, as they take high precedence. If it does not find the property, it looks in the object's own prototype
object recursively until it finds null and throws an error.
A prototype object can inherit from another object via its own prototype. This is called a prototype chain.
console.log(john.hasOwnProperty('name')); // true
console.log(john.hasOwnProperty('print')); // false, it's in the prototype
However, print
works on john
:
john.print(); // "John is 32"
This is why JavaScript is defined as a prototype-based language. We can do more with prototypes than just adding properties and methods, such as inheritance.
The "hello world" of inheritance is the mammal object. Let's recreate it with JavaScript.
// our Mammal blueprint
function Mammal(name) {
this.name = name;
}
Mammal.prototype.breathe = function() {
console.log(`${this.name} is breathing.`);
};
In JavaScript, there's a static function inside the Object
object:
Object.create();
It creates an object similarly to {}
and new functionBlueprint
, but the difference is create
can take a prototype as a parameter to inherit from.
// we use a cat blueprint function here (implemented below)
Cat.prototype = Object.create(Mammal.prototype);
// correction after we inherited all the properties
Cat.prototype.constructor = Cat;
Now Cat
will have the breathe
method found in Mammal
, but the important thing to know is Cat
is pointing to Mammal
as its prototype.
Mammal Blueprint: We first define the Mammal
function and add a breathe
method to its prototype.
Cat Inheritance: We create the Cat
function and set Cat.prototype
to Object.create(Mammal.prototype)
. This makes the Cat
prototype inherit from Mammal
, but it changes the constructor
pointer to Mammal
.
Correcting Constructor: We correct the Cat.prototype.constructor
to point back to Cat
, ensuring that the Cat
object retains its identity while inheriting methods from Mammal
. Finally, we add a meow
method to Cat
.
This approach allows the Cat
object to access methods from both Mammal
(like breathe
) and its own prototype (like meow
).
We need to correct that. Let's create the full example:
function Cat(name, breed) {
this.name = name;
this.breed = breed;
}
Cat.prototype = Object.create(Mammal.prototype); // cat prototype pointing to mammal
// correction after we inherited all the properties
Cat.prototype.constructor = Cat; // we are re-pointing a pointer, the inherited properties are still there
Cat.prototype.meow = function() {
console.log(`${this.name} is meowing.`);
};
To understand the Cat.prototype.constructor = Cat
, you need to know about pointers. When we inherit from Mammal
with Object.create
, it changes the pointer of our Cat
prototype to Mammal
, which is wrong. We still want our Cat
to be its own individual, despite having the parent Mammal
.
That's why we have to correct it.
In this example, Cat
inherits from Mammal
using the prototype chain. The Cat
object can access both breathe
and meow
methods.
const myCat = new Cat("Misty", "Ragdoll");
myCat.breathe(); // Misty is breathing.
myCat.meow(); // Misty is meowing.
We can create a dog also inheriting from the mammal:
function Dog(name, breed) {
this.name = name;
this.breed = breed;
}
Dog.prototype = Object.create(Mammal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} is barking.`);
};
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.breathe(); // Buddy is breathing.
myDog.bark(); // Buddy is barking.
We have created basic classical inheritance, but why is this important? I thought we were covering source code!
Yes, true, but prototypes are the core of building efficient and flexible modules, beyond inheritance. Even simple, well-written modules are ridden with prototype objects. We are just laying the basics.
The alternative to inheritance is object composition, which loosely takes two or more objects and merges them together to form a "super" object.
Mixins allow objects to borrow methods from other objects without using inheritance. They're handy for sharing behavior between unrelated objects.
Which is what our first exploration does: the merge-descriptors
library we promised to cover first.
We have already seen where and how it's used in Express. Now we know it for object composition.
In line 17, here is our first library:
var mixin = require('merge-descriptors');
Usage is in lines 42 and 43:
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
With what we know, we can already deduce that mixin
is composing EventEmitter.prototype
and proto
into an object called app
.
We will get to app
when we start talking about Express.
This is the entire source code for merge-descriptors
:
'use strict';
function mergeDescriptors(destination, source, overwrite = true) {
if (!destination) {
throw new TypeError('The `destination` argument is required.');
}
if (!source) {
throw new TypeError('The `source` argument is required.');
}
for (const name of Object.getOwnPropertyNames(source)) {
if (!overwrite && Object.hasOwn(destination, name)) {
// Skip descriptor
continue;
}
// Copy descriptor
const descriptor = Object.getOwnPropertyDescriptor(source, name);
Object.defineProperty(destination, name, descriptor);
}
return destination;
}
module.exports = mergeDescriptors;
From the beginning, always look at how the function is used and the parameters it takes:
// definition
mergeDescriptors(destination, source, overwrite = true)
// usage
var mixin = require('merge-descriptors');
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
App
is our destination. We know mixin
means object composition. Roughly, what this package is doing is composing the source object into the destination object, with an option to overwrite.
Overwrite, on assumption, is if app
(destination) has an exact property the source has, on true
overwrite, else leave that property untouched and skip.
We know objects cannot have the same property twice. In key-value pairs (objects), keys should be unique.
With Express, the overwrite is false
.
The following is basic housekeeping, always handle expected errors:
if (!destination) {
throw new TypeError('The `destination` argument is required.');
}
if (!source) {
throw new TypeError('The `source` argument is required.');
}
This is where it gets interesting: line 12.
for (const name of Object.getOwnPropertyNames(source)) {
From above, we know what OwnProperty
means, so getOwnPropertyNames
clearly means get the keys of own properties.
const person = {
// data
name: "Jane",
age: 0,
// behavior
grow() {
this.age += 1;
}
};
Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]
It returns the keys as an array, and we are looping over those keys in the following instance:
for (const name of Object.getOwnPropertyNames(source)) {
The following is checking if the destination and source have the same key we are currently looping over:
if (!overwrite && Object.hasOwn(destination, name)) {
// Skip descriptor
continue;
}
If overwrite is false, skip that property; don't overwrite. That is what continue
does—it propels the loop over to the next iteration and doesn't run the code underneath, which is the following code:
// Copy descriptor
const descriptor = Object.getOwnPropertyDescriptor(source, name);
Object.defineProperty(destination, name, descriptor);
We already know what getOwnProperty
means. The new word is descriptor
. Let's test this function on our own person
object:
const person = {
// data
name: "Jane",
age: 0,
// behavior
grow() {
this.age += 1;
}
};
Object.getOwnPropertyDescriptor(person, "grow");
// {
// value: [Function: grow],
// writable: true,
// enumerable: true,
// configurable: true
// }
It returns our grow
function as the value, and the next line is self-explanatory:
Object.defineProperty(destination, name, descriptor);
It's taking our descriptor from the source and writing it into the destination. It's copying source own properties to our destination object as its own properties.
Let's do an example in our person
object:
const val = {
value: function isAlien() { return false; },
enumerable: true,
writable: true,
configurable: true,
};
Object.defineProperty(person, "isAlien", val);
Now person
should have the isAlien
property defined.
In summary, this highly downloaded module copies own properties from a source object into a destination with an option to overwrite.
We have successfully covered our first module in this source code tier, with more exciting stuff to come.
This was an introduction. We started by covering the fundamentals needed to understand the module and, as a byproduct, understand the patterns in most modules, which is object composition and inheritance. Lastly, we navigated the merge-descriptors
module.
This pattern will be prevalent in most articles. If I feel there are necessary fundamentals to cover, we will go through them in the first section and then cover the source code.
Luckily, merge-descriptors
is used in Express, which is our focus as our start source code study. So expect more Express.js source code articles until we feel we have had a good enough run of Express, then switch to another module or tool like Node.js.
What you can do in the meantime as a challenge is navigate to the test file in merge descriptors, read the entire file. Reading source on your own is important, try and figure out what it does and is testing then break it, yes and fix it again or add more tests!
If you are interested in more exclusive practical and longer content to elevate your programming skills, you can find more on Ko-fi.