Pull to refresh

Let's kill all frameworks at once

Level of difficulty Medium
Reading time 14 min
Views 5.2K
My laptop, every time someone is trying to install Postman on it.
My laptop, every time someone is trying to install Postman on it.

The general trend of technology development is characterized by surges and declines. Consider, for instance, the mass movement of human bodies. Initially, horses and wagons were used, which gradually evolved into a distinct industry. Then trains appeared abruptly. Horses were quickly forgotten, and the focus shifted to a new avenue. Steam became an object of study and evolved into a complex science. Diesel and electricity developed concurrently. At a certain point, steam engines became obsolete, and everyone transitioned to diesel and electricity. Similarly, we are now transitioning to electric cars that require significantly fewer fluids.

Technologies evolve and function until new technologies completely replace them. I believe we are entering an era where framework and Electron technologies may be eclipsed by generative AI. Let's examine some examples.

Like our trains and cars, programming languages evolve, function, and become obsolete. During my university education, the emphasis was on the greats of Java and C#. Most of my cohort were committed C# adherents.

After completing our studies and some of us starting work during our second and third years, we quickly realized that language expertise alone proved nothing. One also had to be proficient with development tools and various frameworks. At that time, working at MESI, I started using the trendy Generics feature to write my first DLAC: Data Logic Access Component. In 2005, this was avant-garde. Unlike the brainless PHP projects that were prone to SQL injections due to queries like 'SELECT * from user where name like '.$input.' and enabled = true', we, C# developers, approached things differently. We had classes that wrapped our Entities in proper wrappers, created correct SELECT queries, and allowed for class interactions. All queries were parameterized by default, and since we were developing a university student application, we had to ensure we were not hacked. (Nostalgic MESI students may weep)

On my first job, I practically worshiped these DLACs and couldn't live without them. Over time, I learned about other similar frameworks. In 2009, NHibernate appeared, rewritten in .NET from Hibernate, which had existed since 2001.

Since then, I've closely followed .NET's evolution. I diligently learned new technologies like WCF, WPF, and other version 3.5 wonders. I eagerly awaited the new 4.0 release and installed all updates, meticulously reading the release notes. I was astonished when Scott Hanselman announced that ASP.NET was becoming an open platform with all source code available on GitHub. I was at the forefront of learning .NET…

And then I was introduced to Golang. And I realized I had been the engineer of a good old-fashioned steam locomotive.

The steam engine was sluggishly pulling endless stack traces in my applications. The technologies I was working with were dying, as Microsoft developers themselves ceased support. WPF did not become the new cross-platform standard for UI creation; it was supplanted by the all-encompassing Electron.

Don't get me wrong; initially, I resisted. The absence of classic access modifiers in Go seemed heretical to me. It turned out that many features, like access modifiers, only complicated development without apparent benefit. There was no need to guard class fields as private protected to thwart some negligent programmer from accessing and inadvertently altering them.

It emerged that applications functioned perfectly fine without Dependency Injection. What we had snubbed in 2009 was not complex at all. Simply writing a large main.go and linking all required objects there was not that bad. At least when something breaks, you can always open main.go and select "Go to definition." Eventually, by following the logical chain of code, you'll identify the error. On the other hand, if your program is written following best .NET practices, you'll find yourself, at best, facing some class factory after 20 minutes of digging through config files—a factory that grants you a component based on a dragon-slaying incantation. Moreover, with Go's compilation speed, there was no need to worry about endless configurations. Certain things could simply be hardcoded in main.go and changed along with the program.

I became a steam engine engineer. But not for long. I sat down and learned Go. And afterward—Rust. I learned Node.js and numerous other languages. From all of this, I absorbed one lesson.

No technologies in the computer field are foundational or immutable. Therefore, one should not try to defend any particular technology or say that you will code in it for life. One should simply be a programmer capable of navigating these technologies.

About a year ago, we all experienced another revolution. The world was captivated by ChatGPT. While it struggled with coding tasks initially, by version 4 it had more or less mastered simple tasks.

Now, we are ready to begin discarding useless software and rewriting all this mess.

Take Postman, for example. In my opinion, it is an excellent example of the horror something can become. Postman was written for those engineers who did not understand how to use fetch to test their applications. It then morphed into an endlessly complex machine, where even the devil would break a leg. The only warmth it provides is from the processor of my laptop, struggling to run this bulky binary that takes up more space than my entire folder of torrents.

It's time to replace Postman with curl.

'What?' you say. 'How can you replace such a complex tool with curl?' It's very simple, using ChatGPT.

Let's politely ask the bot to write some code to test our new server.

Certainly, Boss. Below is a Makefile designed to test the specified endpoints with POST requests. It employs curl for HTTP calls and incorporates Basic Authentication.

BASE_URL := {baseUrl}/v1
PASSWORD := Password

    curl -X POST -u $(USERNAME):$(PASSWORD) -H "Content-Type: application/json" -d '{"test": true}' $(BASE_URL)/endpoint1

    curl -X POST -u $(USERNAME):$(PASSWORD) -H "Content-Type: application/json" -d '{"test": true}' $(BASE_URL)/endpoint2

    curl -X POST -u $(USERNAME):$(PASSWORD) -H "Content-Type: application/json" -d '{"test": true}' $(BASE_URL)/endpoint3

test-all: test-endpoint1 test-endpoint2 test-endpoint3

Run individual tests with make test-endpoint1, make test-endpoint2, or make test-endpoint3. To execute all, use make test-all.

Excellent. Now, instead of an entire Postman, we have a single file that can be placed in source control and executed by all your developers. Moreover, it's simple to integrate into a testing system or CI/CD. To add to this,

➜ ~ ls /usr/bin/make /usr/bin/curl -lah
 -rwxr-xr-x 1 root root 271K Jul 17 14:53 /usr/bin/curl
 -rwxr-xr-x 1 root root 250K Feb 15 2022 /usr/bin/make

The combined size of curl and make is 500 kilobytes, compared to Postman's 500 megabytes.

You've just eliminated another Electron-based application that would pester you for money at every opportunity. Furthermore, you don't even have to install anything.

At some point, user demands for software convenience entirely undermined the McIlroy principles.

Doug McIlroy, the inventor of Unix pipes and one of the founders of the Unix tradition, summarized the philosophy as follows:

  1. Write programs that do one thing and do it well

  2. Write programs that work together

  3. Write programs that handle text streams, as it's a universal interface

These statements are usually boiled down to one: "Do one thing but do it well." Of these three principles, only the third is specific to Unix, although Unix developers often emphasize all three.

Unfortunately, all these programs began to sprawl, constantly changing and updating indiscriminately and pointlessly. As a result, I find myself in a quandary trying to check open ports in Linux. nstat? ss? Or something else. I get lost in this motley array of software. Hence, we get Postman, for aesthetics and convenience.

It's becoming overly complicated. And that's exactly where ChatGPT steps in, simplifying the complex.

Yes, a year ago I wouldn't dare to write such a thing in a Makefile. But now it's easy to do because I don't have to fuss with line breaks and tabs. Furthermore, one statement and voilà.

Absolutely, Boss. Below are the test scripts for Bash, PowerShell, and Taskfile.

Bash Script


test_endpoint() {
	curl -X POST -u $USERNAME:$PASSWORD -H "Content-Type: application/json" -d '{"test": true}' $BASE_URL/$1

test_endpoint "endpoint1"
test_endpoint "endpoint2"
test_endpoint "endpoint3"

PowerShell Script

$baseUrl = "{baseUrl}/v1"
$username = "Bill"
$password = "Password"

Function Test-Endpoint ($endpoint) {
	Invoke-RestMethod -Uri "$baseUrl/$endpoint" -Method POST -Headers @{ "Content-Type" = "application/json" } -Body '{"test": true}' -Credential (New-Object System.Management.Automation.PSCredential ($username, (ConvertTo-SecureString $password -AsPlainText -Force)))

Test-Endpoint "endpoint1"
Test-Endpoint "endpoint2"
Test-Endpoint "endpoint3"


Create a file named Taskfile.yml and add the following content:

version: '3'

  	- 'curl -X POST -u Bill:Password -H "Content-Type: application/json" -d ''{"test": true}'' {baseUrl}/v1/endpoint1'
  	- 'curl -X POST -u Bill:Password -H "Content-Type: application/json" -d ''{"test": true}'' {baseUrl}/v1/endpoint2'
  	- 'curl -X POST -u Bill:Password -H "Content-Type: application/json" -d ''{"test": true}'' {baseUrl}/v1/endpoint3'
	deps: [test-endpoint1, test-endpoint2, test-endpoint3]

Run individual tasks with task test-endpoint1, task test-endpoint2, or task test-endpoint3. To execute all, use task test-all.

Here you have Postman in Taskfile, PowerShell, and Bash.

Does this file accomplish EVERYTHING Postman does? No, not even close. However, it does achieve approximately 97% of its functions within seconds, without installing additional software.

Now, let's consider what else we can part ways with.

Let's take an area that is the epitome of bloated complexity in programming: web development. Who isn't ranting and raving about how the web has turned into a horrific mess, filled with millions of frameworks and redundancies?

So, I sit here in front of an application written in Nest.js. Nest represents the kind of needless complexity created for unknown reasons. The documentation is pitiful ("a cat could cry," their website filled with crying cat emojis), and much of it is deliberately designed to entice you to purchase a commercial license with support. The framework was built to introduce complexity where none should exist, and then to monetize the untangling of that complexity. Earlier this year, I received a project with a request to fix one function and deploy everything on AWS.

I spent two days searching for the function that needed modification. Everything is obfuscated by decorators, stores, and other trivia borrowed from React; it's a veritable labyrinth. There's no central file to consult to identify the issue—only chaos exists.

We know that the key to project delivery is the ability to change a button size by two pixels and recolor anything that can be colored. Nest decided that CSS is unworthy for a civilized individual and should be replaced by themes. All well and good, but come delivery time, they want to change the color of a button. Do you think it's straightforward in Nest?

Alright, ChatGPT, do your job.

Certainly, Boss. Rendering TSX without React can be achieved using a custom JSX factory function. Below is a minimal TypeScript script that provides a way to render simple TSX files.

Create a tsx-renderer.ts file:

const createElement = (tag: string, props: any, ...children: any[]) => {
  let element = document.createElement(tag);
  for (let key in props) {
	element.setAttribute(key, props[key]);
  children.forEach((child) => {
	if (typeof child === "string") {
	} else {
  return element;

// Define JSX factory in global scope
(globalThis as any).React = { createElement };

// Sample TSX content
const App = () => {
  return (
	<div id="app">
  	<h1>Hello, world!</h1>

// Render to document body

Configure tsconfig.json:

  "compilerOptions": {
	"target": "ES6",
	"module": "commonjs",
	"jsx": "react",
	"jsxFactory": "React.createElement"
  "include": ["*.ts", "*.tsx"]

This script allows you to render TSX by utilizing a custom JSX factory function. Run the TypeScript compiler (tsc) to transpile this to JavaScript, and execute it in a browser context.

What? You mean to say I can actually render TSX without React, just by using this script? Meaning, I don't have to install CRA with 1,500 dependencies? How?

You might reasonably argue that this script can't replace all of React (which Nest runs on). Of course not! But it will replace 80% of what I need from React. The rest I will code manually.

But that's too much boilerplate! Yes, but I won't be the one writing it.

But ChatGPT sometimes makes mistakes, and even in the examples you provided, there are errors! You are absolutely correct. I will fix these errors and learn how everything works. I'll understand the internal mechanics.

Now, let's bring our next convict to the firing squad: Bootstrap.

I require a CSS file complete with CSS-reset, rules for both dark and light themes, as well as a grid container.

Certainly, Boss. Here's the optimized CSS file incorporating dynamic theme switching based on user preferences.

/* CSS Reset */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;

/* Base Variables */
:root {
  /* Light Theme */
  --light-bg-lighter: #f5f5f5;
  --light-bg: #ffffff;
  --light-bg-darker: #e0e0e0;
  --light-accent-orange: #ff9800;
  --light-accent-purple: #673ab7;
  --light-gradient: linear-gradient(to right, #ff9800, #ff4081, #b71c1c, #673ab7);
  /* Dark Theme */
  --dark-bg-lighter: #424242;
  --dark-bg: #303030;
  --dark-bg-darker: #212121;
  --dark-accent-orange: #ff9800;
  --dark-accent-purple: #673ab7;
  --dark-gradient: linear-gradient(to right, #ff9800, #ff4081, #b71c1c, #673ab7);

  /* Default to Light Theme */
  --bg-lighter: var(--light-bg-lighter);
  --bg: var(--light-bg);
  --bg-darker: var(--light-bg-darker);
  --accent-orange: var(--light-accent-orange);
  --accent-purple: var(--light-accent-purple);
  --gradient: var(--light-gradient);

/* Automatic Theme Switch */
@media (prefers-color-scheme: dark) {
  :root {
	--bg-lighter: var(--dark-bg-lighter);
	--bg: var(--dark-bg);
	--bg-darker: var(--dark-bg-darker);
	--accent-orange: var(--dark-accent-orange);
	--accent-purple: var(--dark-accent-purple);
	--gradient: var(--dark-gradient);

/* Mobile-First Base Styles */
body {
  font-size: 16px;
  background-color: var(--bg);
  font-family: 'Arial', sans-serif;
  line-height: 1.6;

/* Grid System */
.container {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;

@media (min-width: 768px) {
  .container {
	grid-template-columns: 1fr 1fr;

@media (min-width: 1024px) {
  .container {
	grid-template-columns: 1fr 1fr 1fr;

/* Typography */
h1, h2, h3, h4, h5, h6 {
  margin-bottom: 16px;

h1 {
  font-size: 2em;

h2 {
  font-size: 1.5em;

h3 {
  font-size: 1.3em;

h4 {
  font-size: 1.2em;

h5 {
  font-size: 1.1em;

h6 {
  font-size: 1em;

p {
  margin-bottom: 16px;
  color: var(--bg-darker);

This version utilizes CSS variables and media queries for automatic theme switching based on user preferences, in addition to the typography and header stipulations.

Is that all? I'll have a splendid grid for layout within this grid? Wait, why so simple? Because when the first Bootstrap was released, we were witnessing the second browser war, and IE6 was still lingering in some places. But now, we don't have to deal with that. We can put everything into a grid, and have a layout grid.

Again, a few things will need to be manually adjusted, but you have a file measured in kilobytes instead of the boundless Bootstrap.

But that's not all! You're correct. Button themes and the like will be handled by GPT. I gain a tool entirely under my control.

But wait, why not get rid of this TypeScript? It's just a nuclear concoction pretending to be a strongly-typed programming language. Just try parsing a JSON into a strongly-typed object with incorrect field types. You'll roar like an elephant, trying to understand why 1 !== 1.

Certainly, Boss. Below is the Vugu code that will accomplish the described tasks. Please make sure you have the necessary setup for a Vugu project.

   		 <input type="number" @input="CalcSum" v-model="Num1" />
   		 <input type="number" @input="CalcSum" v-model="Num2" />
   		 <div>{{ Sum }}</div>

   		 First Name: <input type="text" v-model="FirstName" @input="GenerateMessage" />
   		 Last Name: <input type="text" v-model="LastName" @input="GenerateMessage" />
   		 Birthday: <input type="date" v-model="Birthday" @input="GenerateMessage" />
   		 <div>{{ Message }}</div>

import "time"

type MyComponent struct {
    Num1  	float64
    Num2  	float64
    Sum   	float64
    FirstName string
    LastName  string
    Birthday  string
    Message   string

func (c *MyComponent) CalcSum(event vugu.DOMEvent) {
    c.Sum = c.Num1 + c.Num2

func (c *MyComponent) GenerateMessage(event vugu.DOMEvent) {
    birthdayTime, _ := time.Parse("2006-01-02", c.Birthday)
    secondsOld := time.Now().Sub(birthdayTime).Seconds()
    c.Message = fmt.Sprintf("Hello %s %s! You are %.0f seconds old!", c.FirstName, c.LastName, secondsOld)

Make sure to adapt this code according to your specific Vugu project configuration and setup. If you need further assistance, please let me know.

Introducing code in Vugu, a rather subdued Golang framework that allows you to write WebAssembly code directly in Golang.

But what are we, confined solely to Golang? Why not rework this in Rust:

Certainly, Boss. Below is an example using Yew, a popular Rust framework for building web applications with WebAssembly.

use yew::{html, Component, ComponentLink, Html, ShouldRender};
use yew::services::ConsoleService;
use chrono::prelude::*;

pub struct App {
	num1: f64,
	num2: f64,
	sum: f64,
	first_name: String,
	last_name: String,
	birthday: NaiveDate,
	message: String,

pub enum Msg {

impl Component for App {
	type Message = Msg;
	type Properties = ();

	fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
    	App {
        	num1: 0.0,
        	num2: 0.0,
        	sum: 0.0,
        	first_name: String::new(),
        	last_name: String::new(),
        	birthday: NaiveDate::from_ymd(2000, 1, 1),
        	message: String::new(),

	fn update(&mut self, msg: Self::Message) -> ShouldRender {
    	match msg {
        	Msg::Num1Changed(val) => {
            	self.num1 = val.parse().unwrap_or(0.0);
            	self.sum = self.num1 + self.num2;
        	Msg::Num2Changed(val) => {
            	self.num2 = val.parse().unwrap_or(0.0);
            	self.sum = self.num1 + self.num2;
        	Msg::FirstNameChanged(val) => {
            	self.first_name = val;
        	Msg::LastNameChanged(val) => {
            	self.last_name = val;
        	Msg::BirthdayChanged(val) => {
            	self.birthday = NaiveDate::parse_from_str(&val, "%Y-%m-%d").unwrap();

    	let now = Local::now().naive_utc().date();
    	let duration = now.signed_duration_since(self.birthday);
    	let seconds = duration.num_seconds();

    	self.message = format!(
        	"Hello {}, {}! You are {} seconds old!",
        	self.first_name, self.last_name, seconds


	fn view(&self) -> Html {
    	html! {
                	<input type="number" value=self.num1.to_string() oninput=self.link.callback(|e: InputData| Msg::Num1Changed(e.value)) />
                	{" + "}
                	<input type="number" value=self.num2.to_string() oninput=self.link.callback(|e: InputData| Msg::Num2Changed(e.value)) />
                	<div>{ self.sum }</div>

                	<span>{"First Name: "}</span>
                	<input type="text" value=&self.first_name oninput=self.link.callback(|e: InputData| Msg::FirstNameChanged(e.value)) />
                	<span>{"Last Name: "}</span>
                	<input type="text" value=&self.last_name oninput=self.link.callback(|e: InputData| Msg::LastNameChanged(e.value)) />
                	<span>{"Birthday: "}</span>
                	<input type="date" value=self.birthday.format("%Y-%m-%d").to_string() oninput=self.link.callback(|e: InputData| Msg::BirthdayChanged(e.value)) />
                	<div>{ &self.message }</div>

Make sure to add dependencies for yew and chrono in your Cargo.toml file. This code should deliver on the requested functionality with precision and efficiency. Should you need further details, I stand ready to assist.

Errors? Yes! Doesn't compile? Of course! After all, this is written not by a diligent programmer, but by a machine that neither has a compiler nor a sense of responsibility for such code. But we can correct this manually, can we not?

The era of hiding from boilerplate behind walls of endless frameworks is over. We no longer need stack traces that lead nowhere. You can simply have GPT generate the code you need. If something is off, you can amend it. All your code will reside in a repository, and any required changes can be made without sifting through extraneous documentation.

Ever since ChatGPT appeared, my repositories have had more Bash scripts. Initially, my colleagues questioned my hesitance toward adopting yet another modern framework. Their opinion has since changed.

I had to write a server to replace the old one on Next.js as we were moving from Vercel to AWS. The old server kept crashing. I wrote the new server in pure TS. Currently, my pnpm ls looks like this.

  • @prisma/client 4.8.1

  • body-parser 1.20.1

  • cors 2.8.5

  • express-basic-auth 1.2.1

  • jsonwebtoken 9.0.0

  • otplib 12.0.1

  • prisma-json-types-generator 2.2.0

  • uuid 9.0.0

  • bcrypt 5.1.0

  • cookie-parser 1.4.6

  • dotenv 16.0.3

  • express 4.18.2

  • google-auth-library 8.7.0

  • oauth-1.0a 2.2.6

  • typescript 4.9.4

  • zxcvbn 4.4.2

The gross mistake I made was using Express when a simple httplisten would suffice. I am currently extracting it from the project. Crypto modules will remain untouched due to obvious risks.

Handing this project to another developer initially met resistance. He later asked, "So I can open main.ts and trace every call from there?" I confirmed. He was delighted. This was self-documenting code.

Extremes lead to madness. You can have an ice cream, or you can attempt to rob an ice cream factory to eat all the ice cream in the world. The first is not madness; the second is.

I am not advocating a return to basics to the extent of writing web applications in pure Assembly. However, bulky node-modules can be left in the past. GPT can rewrite that missing left-pad for you.

Rating 0
Comments 8
Comments Comments 8