Skip to content

Sloppy vs Node, Bun, and Deno

Sloppy is not a compatibility layer for Node, Bun, or Deno. It explores a different shape: compiler-first TypeScript apps with a native runtime and first-party backend APIs.

Node, Bun, and Deno run JavaScript programs. Sloppy compiles supported TypeScript source into a Plan first, then runs the known app shape on a native runtime. That makes Sloppy stricter in some places, but it gives the runtime and tooling more information before execution.

The short version

Node.js, Bun, and Deno are general JavaScript runtimes. They are good choices when you need the JavaScript ecosystem, dynamic program structure, existing frameworks, or mature deployment paths.

Sloppy is a compiler-first backend runtime and app host. A Sloppy app imports the Sloppy stdlib, stays inside the compiler-supported source subset, and emits artifacts that the runtime validates before serving work:

  • app.plan.json — route, handler, capability, provider, config, response, and artifact metadata;
  • app.js — generated bundle;
  • app.js.map — source map for diagnostics.

Sloppy favors first-party backend/runtime APIs and sealed artifact graphs. Compatible installed pure-JavaScript packages can be bundled when they fit Sloppy's resolver, loader, and runtime API boundary. The trade is direct: less dynamic freedom, more app metadata available before the first request.

A tiny route in each runtime

These examples intentionally use each runtime's built-in surface, not a third-party framework. Bun includes a built-in router, so the Bun example uses it. Node and Deno examples use their built-in HTTP server APIs directly and therefore do route matching manually.

Sloppy

ts
import { Sloppy, Results } from "sloppy";

const app = Sloppy.create();

app.get("/health", () => Results.text("ok"));
app.get("/hello/{name}", (ctx) =>
    Results.json({ hello: ctx.route.name })
);

export default app;

Run it as a dev server:

sh
sloppy run src/main.ts

Or run one synthetic smoke request and exit:

sh
sloppy run src/main.ts --once GET /hello/Ada

The first command starts the HTTP listener. The second command compiles, validates, dispatches GET /hello/Ada, writes the response, and exits.

Node.js

js
const http = require("node:http");

const server = http.createServer((req, res) => {
  const url = new URL(req.url, "http://localhost");

  if (req.method === "GET" && url.pathname === "/health") {
    res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
    res.end("ok");
    return;
  }

  const match = url.pathname.match(/^\/hello\/([^/]+)$/);
  if (req.method === "GET" && match) {
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ hello: decodeURIComponent(match[1]) }));
    return;
  }

  res.writeHead(404);
  res.end();
});

server.listen(5173, "127.0.0.1");

Run:

sh
node server.js

Bun

js
Bun.serve({
  port: 5173,
  hostname: "127.0.0.1",
  routes: {
    "/health": () =>
      new Response("ok", {
        headers: { "content-type": "text/plain; charset=utf-8" },
      }),

    "/hello/:name": (req) =>
      Response.json({ hello: req.params.name }),
  },
  fetch() {
    return new Response(null, { status: 404 });
  },
});

Run:

sh
bun server.js

Deno

ts
Deno.serve({ port: 5173, hostname: "127.0.0.1" }, (request) => {
  const url = new URL(request.url);

  if (request.method === "GET" && url.pathname === "/health") {
    return new Response("ok", {
      headers: { "content-type": "text/plain; charset=utf-8" },
    });
  }

  const match = url.pathname.match(/^\/hello\/([^/]+)$/);
  if (request.method === "GET" && match) {
    return Response.json({ hello: decodeURIComponent(match[1]) });
  }

  return new Response(null, { status: 404 });
});

Run:

sh
deno run --allow-net server.ts

What Sloppy knows before runtime

In Node, Bun, and Deno, framework metadata usually comes from executing app registration code or from framework-specific tooling. Sloppy's compiler records the supported app shape in the Plan.

For the route above, a current Plan contains fields like this. This excerpt is illustrative, but it uses current field names:

json
{
  "schemaVersion": 1,
  "kind": "web",
  "routes": [
    {
      "method": "GET",
      "pattern": "/hello/{name}",
      "handlerId": 2,
      "response": {
        "kind": "json",
        "status": 200
      },
      "responses": [
        {
          "status": 200,
          "kind": "json",
          "contentType": "application/json"
        }
      ]
    }
  ],
  "requiredFeatures": ["stdlib"]
}

Depending on the source shape, the Plan can also carry:

  • handler IDs and source locations;
  • route params, query/header/body bindings, and request context usage;
  • config reads and literal defaults;
  • required runtime features such as filesystem, network, OS, time, crypto, codec, workers, and provider features;
  • provider and capability metadata;
  • response metadata visible from Results.*;
  • OpenAPI-relevant operation metadata;
  • metadata completeness markers.

The runtime loads and validates that metadata before handler execution. CLI commands can also read it without entering V8.

Developer ergonomics

Sloppy can feel better when you want the runtime and tools to understand the app shape up front:

  • one first-party app surface: Sloppy.create(), route registration, middleware, services, config, logging, capabilities, and Results;
  • route metadata without runtime introspection;
  • OpenAPI from Plan metadata;
  • Plan-backed commands such as sloppy routes, sloppy capabilities, sloppy doctor, sloppy audit, and sloppy openapi;
  • source input with sloppy run src/main.ts;
  • artifact runs with sloppy run .sloppy;
  • local packages with sloppy package;
  • Program Mode for route-free tools that use the Sloppy stdlib.

The costs are real:

  • Sloppy can bundle compatible installed pure-JavaScript packages, but it does not install packages, solve versions, or load native Node addons.
  • Node built-ins and globals exist only through explicit Sloppy APIs or compatibility shims.
  • The compiler accepts a focused TypeScript/JavaScript source subset.
  • Dynamic web app shapes can run, but metadata may be partial. Sloppy shows the incomplete parts instead of pretending they are complete.
  • Sloppy is public alpha software. APIs, artifact formats, and internal boundaries can still change.

Use this rule of thumb: Sloppy is a good fit when you want compiler-first backend metadata and can stay inside Sloppy's explicit runtime boundary. Node, Bun, and Deno are better fits when you need the full JavaScript ecosystem, maximum dynamic freedom, or mature production deployment today.

CLI ergonomics

Sloppy's CLI reads source, artifacts, and packages through the same Plan contract:

sh
sloppy run src/main.ts
sloppy routes .sloppy
sloppy openapi .sloppy --output openapi.json
sloppy package src/main.ts

What each command reads:

CommandReadsPurpose
sloppy run src/main.tssource, then emitted artifactscompile, validate, and execute
sloppy run .sloppyapp.plan.json, app.js, app.js.mapvalidate and execute existing artifacts
sloppy routes .sloppyPlanlist route metadata without V8
sloppy openapi .sloppy --output openapi.jsonPlangenerate OpenAPI JSON for web Plans
sloppy package src/main.tssource, then artifactscreate a local package directory

Node, Bun, and Deno run files directly and have strong ecosystem tooling. They do not have Sloppy's Plan-backed metadata commands by default because there is no Sloppy Plan. Frameworks and external tools can add similar workflows, but that metadata is framework-specific rather than emitted by the runtime's own compiler step.

Program Mode

Sloppy is not only for web routes. Program Mode runs route-free source as a console-style program:

ts
export async function main(args, ctx) {
    console.log(`hello ${args[0] ?? "world"}`);
}

Run it with arguments after --:

sh
sloppy run src/main.ts -- Ada

Program Mode is for Sloppy stdlib imports, compatible bundled package imports, Plan-backed packaging, and runtime metadata. It is not full Node compatibility. There is no global Node process, native addon loading, FFI, or raw terminal API in the current Program Mode surface. Node builtins are limited to the explicit compatibility registry.

Node, Bun, and Deno are more general-purpose program runners. Sloppy Program Mode is useful when the tool should use the same compiler/runtime/package boundary as a Sloppy app.

Comparison table

AreaSloppyNode.jsBunDeno
Runtime modelCompiler-first app Plan + native runtimeGeneral JavaScript runtimeGeneral JavaScript runtime and toolkitSecure-by-default JavaScript/TypeScript runtime
Package ecosystemFirst-party stdlib plus compatible bundled installed JavaScript packagesMature npm ecosystemStrong npm supportnpm support plus URL/import-map workflows
Web metadataFirst-party Plan, routes, OpenAPIApp/framework-specificApp/framework-specificApp/framework-specific
Dynamic JavaScript freedomIntentionally limited in Web ModeHighHighHigh
Program/CLI appsProgram Mode via Sloppy stdlibMatureMatureMature
Node built-insExplicit Sloppy APIs and partial node:* shimsNative surfaceBroad supportAvailable through compatibility surfaces where supported
Production maturityPublic alphaMatureMatureMature

When to use Sloppy

Use Sloppy if:

  • you want compiler-first backend metadata;
  • you want an integrated CLI, app host, and first-party stdlib;
  • you like ASP.NET Minimal API-style route ergonomics;
  • you want to experiment with Plan-backed tooling;
  • you can build inside Sloppy's explicit runtime boundary.

Use Node, Bun, or Deno if:

  • you need a mature production runtime today;
  • you need broad npm compatibility or native addons inside the app today;
  • you need full Node built-ins or framework compatibility;
  • you need arbitrary dynamic JavaScript behavior;
  • you are porting an existing JavaScript backend without rewriting its runtime assumptions.

Public alpha. APIs and artifact formats may still change between alpha revisions.