As kids, I believe we all one day want to become something; some seemly unattainable goal. For me, personally, like many others - I wanted to be a socially-responsible developer. It's the unknown to me.

Socially-responsible developer?

Social responsibility is an ethical framework and suggests that an entity, be it an organization or individual, has an obligation to act for the benefit of society at large. Social responsibility is a duty every individual has to perform so as to maintain a balance between the economy and the ecosystems.

In other words, you will build ethical software that is beneficial to society. It's important to note, you don't have to target the entire world, just one thing you're passionate about.

The Idea

We've got a world full of chat applications, robots, machine-learning AI, and dozens of other tools at our disposal, communities thriving every single day with innovation.

My project isn't by any means of the word fancy, it's a backend framework written in Node and Typescript. I get it, we've got a lot of them already. There are already some brilliant generalist frameworks, but these are generalist frameworks. They're designed to fit a generic purpose - the "all-in-one" use case.

This backend framework is envisioned to be lightweight, robust, but strict: everything is an immutable object, everything constructs messages, these messages can contain actions such as patching, deleting objects or database records. This server will run Express under the hood, with a built-in View Wrapper (not an engine: more on this below) that can adapt to the individuals needs, helping to prevent vendor lock-in.

Let's break down some of the concepts described above!

Lightweight, robust but strict

An interesting mix to begin with. We want to chop out the bulkiness of frameworks and dependencies, giving you a bare-bones setup, you must explicitly import the function you need. This helps reduce runtime weight, and makes the developers aware exactly what is required to get the application running, and helps make the application more robust to the end-user.

By enforcing a very strict policy, we make everything fully typed, to help reduce errors, catching them at compile time. Furthermore, all tests should be fully typed, and be able to interact with pieces of the application, without having to import the whole application stack to test it - essentially, a fully modular application.

Messages, Actions and Patching

A unified workflow for applications happens with Messages. Messages and Actions will define exactly how the application will behave. Suppose, we have two objects, Person and Animal. Let's construct a sample structure for them:

import PersonInterface from '@kubydev/framework-saas/interfaces/Person';
import AnimalInterface from '@kubydev/framework-saas/interfaces/Animal';

class Person implements PersonInterface {
    public name: string  = "Guest";
    public age:  number  = 21;
    public email: string = "[email protected]";

class Animal implements PersonInterface {
    public name: string  = "Dog";
    public age:  number  = 1;
    public stage: string = "puppy";

We can see from here, we've got two explicit classes, in reality we'd have some setters/getters where applicable, but let's now implement a sample Message and Action that should "patch" the Person interface, setting it's name to "Alice", this could look like this:

import Message from '@kubydev/framework-saas/interfaces/Message';
import Action  from '@kubydev/framework-saas/enums/Action';
import Patcher from '@kubydev/framework-saas/src/classes/Patcher';

let Alice = new Person();

// Set our desired base object in the Message constructor, and optional flags with limitations of this Action

const Request = new Message( Alice, Action.CAN_UPDATE, Action.CAN_DESTROY );

Request.set('name', 'Alice'); // This can also trigger Person-specific setter if applicable, with modified syntax

Alice = Patcher.apply( Alice, Request ); // We now want to "apply" our changes from our Request into a new version of Alice

Now, if we were to output the content of Alice, we would see the "name" attribute was changed from "Guest" to "Alice" - it is important to note in a real situation, you would likely have multiple classes interacting with shared objects, this is the design of the Patcher class, allowing the staging of objects, and applying them respectively.

The Patcher interface is also limited by the flags declared, if you declare a flag like CANNOT_UPDATE, this will let you manipulate the Request object, but never apply it to the Alice class. That's rather confusing, so breaking it down in a code example, it's looking like this:

// create our "Alice" person object
let Alice = new Person();

// Let's create a new Message request, with our object, and flags of CANNOT_UPDATE with CAN_MANIPULATE_LOCAL_DATA
const Request = new Message( Alice, Action.CANNOT_UPDATE, Action.CAN_MANIPULATE_LOCAL_DATA );

Alice<Person> {
  name: "Guest";
  age: 21;
  email: "[email protected]";

Request.set('name', 'Alice').set('age', 24).set('email', '[email protected]');

Request<Alice<Person>> {
    name: "Alice";
    age: 24;
    email: "[email protected]";

// Next we're going to try to "UPDATE" the object with the patcher, this WILL throw an error on compile, as you can't update an object with the CANNOT_UPDATE flags, this will also prevent shared objects that use this request from committing the changes

Alice = Patcher.apply( Alice, Request );

<error>: Patcher<class>::apply ( origin_object: ActionableClass, next_object: ActionableClass ) cannot compile, flag CANNOT_UPDATE is present and will not write to next_object

Thus, meaning you can only manipulate an in-memory copy of the class. The unique things about requests, is despite the inner class being mutable, the initial Message configuration is immutable, if you flag CANNOT_UPDATE, you cannot remove the flag on the fly, this is by design.

View Wrapper

With dozens of incredible view libraries, why re-invent the wheel for this framework? The goal is this framework isn't to implement a whole view engine, instead, a lightweight server-side renderable library will be optional to include, it packs either Next, Nuxt, or a generic mustache template compiler. Nothing fancy, the only interesting part is that controllers can define multiple rendering engines: for example, if you had a frontend view mixed with React, Vue, and Mustache you can define "View Blocks" that will set the render engine.

View Blocks

The view blocks just help the framework understand which libraries to include when rendering to the client, a sample view block looks like this:

import ViewBlock from '@kubydev/framework-saas/view-wrapper/blocks/ViewBlock';
import NextRender from '@kubydev/framework-saas/view-wrapper/engines/React/Next';
import MustacheEngine from '@kubydev/framework-saas/view-wrapper/engines/Mustache/Engine';

export class ViewTest extends ViewBlock {
    public renderEngine: ViewBlock.ViewEngine = new NextRender();

export class ViewOther extends ViewBlock {
    public renderEngine: ViewBlock.ViewEngine = new MustacheEngine();

export class Structure extends ViewBlock {
    public map( ) {
        return this.match( 'Other', new ViewOther() ).match( 'Test', new ViewTest() );

A little busy, but we defined two sample render classes, and a structure class which maps the blocks. Now, if we have a tag in our view that begins with <Other>, it'll use the Mustache engine, and <Test> will use our React engine. These will be pulled out and compiled isolated of each other, and loaded with state information defined in the controller before rendering.

Development Progress

With a clear roadmap in mind, development has already begun, the setup of proper interfaces and class structure is there. This is still a working draft, so I fully expect paths to change as this project progresses, but I'm looking forward to the future of this project. Let it all begin now!

Psst, I've got a patreon, if you like my content feel free to check it out!

saas engineering


Senior Software Engineer, Labber, Sysadmin. I make things scale rapidly. Optimize everything.

Read More