Compare

Geblang vs Node.js

Keep the async and await you already use and gain types that are checked on real data at run time, async tasks in parallel on every CPU core, a batteries-included standard library, and a single executable instead of node_modules.

Geblang and Node.js at a glance

Node.jsGeblang
TypesTypeScript checks types when compiling, then erases them; validating data at run time needs a schema library such as zodTypes are checked before the program runs and enforced at run time, including on parsed JSON and request bodies
null and undefinedTwo absent values; strictNullChecks helps at compile time onlyOne null, allowed only in types declared nullable with ?
ConcurrencyJavaScript runs on one event-loop thread per process; worker_threads add parallelism with message passingAsync tasks run on goroutines across every core, with channels and select
Standard libraryA core library, with databases, YAML, validation, and most else from npmHTTP and WebSockets, SQL drivers, Redis, data formats, crypto, logging, metrics, queues, and LLM clients built in
DeploymentNode.js runtime plus node_modules, or a bundling stepgeblang build produces one self-contained executable

Typed JSON that is actually checked

In TypeScript, as User is a promise to the compiler, not a check. Malformed input passes straight through and fails later, far from where it entered.

Node.js

interface User {
  name: string;
  age: number;
}

const user = JSON.parse('{"name": "Devi", "age": "unknown"}') as User;
console.log(user.age.toFixed(0)); // compiles, then throws at run time

Geblang

import io;
import json;

class User {
    string name;
    int age;
}

let user = json.parseAs("{\"name\": \"Mara\", \"age\": 34}", User);
io.println("${user.name} is ${user.age}");

try {
    json.parseAs("{\"name\": \"Devi\", \"age\": \"unknown\"}", User);
} catch (TypeError e) {
    io.println("rejected: " + e.message);
}

json.parseAs builds a real User and checks every field against its declared type, so bad input is rejected where it arrives, with the field named. See data formats.

Request validation without a schema library

In an Express service, the TypeScript type and the runtime schema are two descriptions of the same data, kept in step by hand or derived with z.infer.

Node.js (Express and zod)

const Signup = z.object({ email: z.string(), age: z.number() });

app.post("/signup", (req, res) => {
  const result = Signup.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json(result.error);
  }
  res.status(201).json({ welcome: result.data.email });
});

Geblang (Gebweb)

import gebweb;
import http;
import io;

class Signup {
    string email;
    int age;
}

@Controller("/signup")
class SignupController extends gebweb.Controller {
    @Post("")
    func create(Signup body): http.Response {
        return this.json({"welcome": body.email}, 201);
    }
}

let client = gebweb.TestClient(gebweb.app());
let ok = client.post("/signup", {"email": "[email protected]", "age": 36});
io.println("${ok.status} ${ok.text()}");
let bad = client.post("/signup", {"email": "[email protected]", "age": "old"});
io.println("${bad.status} ${bad.json()["detail"]}");

The class is the schema: Gebweb binds the body to Signup, enforces its field types, and answers a mismatch with a 400 that names the field. The same class feeds the generated OpenAPI document. See web development with Gebweb.

null that you opt in to

JavaScript has both null and undefined, and a TypeScript string field can still receive null from parsed input. In Geblang a type holds null only when it is declared nullable.

Geblang

import io;
import json;

class User {
    string name;
    ?string email;
}

func contact(User user): string {
    return user.email ?? "no email for ${user.name}";
}

io.println(contact(json.parseAs("{\"name\": \"Lin\", \"email\": null}", User)));

try {
    json.parseAs("{\"name\": null, \"email\": \"[email protected]\"}", User);
} catch (TypeError e) {
    io.println("rejected: " + e.message);
}

?? supplies a fallback for nullable values, and a null arriving in a non-null field is rejected where it enters. See nullability.

CPU-bound work without blocking the event loop

A CPU-heavy calculation on the main thread stalls every other request. Moving it off the event loop means worker threads, separate files, and message passing.

Node.js

const { Worker } = require("node:worker_threads");

// Each worker runs a separate script and returns its result
// through postMessage; shared state needs SharedArrayBuffer.
const worker = new Worker("./count-primes.js", { workerData: 20000 });
worker.on("message", (count) => console.log(count));

Geblang

import async;
import io;

func countPrimes(int limit): int {
    int count = 0;
    for (n in 2..limit) {
        bool prime = true;
        int d = 2;
        while (d * d <= n) {
            if (n % d == 0) {
                prime = false;
                break;
            }
            d += 1;
        }
        if (prime) {
            count += 1;
        }
    }
    return count;
}

let tasks = [async.run(func(): int { return countPrimes(20000); }) for _ in 0..<4];
io.println(await async.all(tasks));

In Geblang the same function runs as four parallel tasks in one process, and the results come back with await. See async and generators.

Batteries included, one executable

A typical Node.js service pulls dozens of npm packages for its database driver, YAML, validation, logging, and metrics. Geblang's standard library covers an HTTP client and server with WebSockets, SQLite, PostgreSQL, and MySQL, Redis, JSON, YAML, TOML, XML, and CSV, cryptography, structured logging, metrics and tracing, message queues, and LLM clients.

geblang build then bundles your code, its dependencies, and the runtime into one file, with no node_modules to ship and no runtime to install. See bundling and builds.

geblang build --entry app --out dist/app
./dist/app

Which should you choose?

Choose Geblang when:

  • You want runtime type safety on real data without adding a validation library.
  • CPU-bound work should run in parallel without separate worker scripts.
  • You would rather rely on a broad standard library than a large dependency tree.
  • You want one executable instead of a runtime and a node_modules tree.

Node.js still makes sense when you share code and types with a browser front end, rely on npm packages with no equivalent elsewhere, or run a mostly I/O-bound workload the event loop already serves well.

Frequently asked questions

Does Geblang check JSON at run time like zod does?

Yes, without a schema library. json.parseAs builds an instance of your class and checks every field against its declared type, and the Gebweb framework applies the same checks to request bodies, answering with a 400 that names the field.

Does Geblang have async and await?

Yes, with the same keywords. Unlike Node.js, async tasks run in parallel on every CPU core, and channels and select coordinate them.

Can Geblang share code with a browser front end?

No. Geblang runs on servers and the command line and does not compile to JavaScript. If sharing code and types with a browser front end is essential, TypeScript is the better fit.

What does Geblang include instead of npm packages?

A broad standard library: an HTTP client and server with WebSockets, SQLite, PostgreSQL, and MySQL drivers, Redis, JSON, YAML, TOML, XML, and CSV, cryptography, structured logging, metrics and tracing, message queues, templates, and LLM clients. Third-party Geblang packages install from Git with geblang install.

How do I deploy a Geblang service?

Run geblang build to produce one self-contained executable that bundles your code, its dependencies, and the runtime. There is no node_modules directory to ship and no runtime to install on the server. Add --docker to also write a Dockerfile.