Hello World with NDL: Node.js and Deno


One of NDL’s core promises is polyglot support: the same workflow works whether you’re using Node.js, Deno, .NET, or any other runtime.

This article walks through two minimal “Hello World” demos—one with Node.js, one with Deno—to show how NDL handles different runtimes with the same declarative approach.

The key insight: the manifest only changes in one line (the command). Everything else—DNS-based service names, gateway routing, port templating—stays exactly the same.

The Node.js Version

Here’s a minimal Node.js HTTP server:

const http = require("http");

const PORT = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello Node from NDL!\n");
});

server.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}/`);
});

Nothing special—just a standard Node.js server that reads PORT from the environment.

The Manifest

Here’s the NDL manifest that deploys it:

# Simple Node.js Hello World - NDL Demo
#
apiVersion: apps/v1alpha1
kind: Deployment
metadata:
  name: hello-node
  namespace: default
spec:
  source:
    workingDirectory: "."
    command: "node server.js"
    environment:
      PORT: "{{.Service.Port}}"
      NODE_ENV: "development"
  service:
    port: 3000

Key details:

  • command: "node server.js" — tells NDL how to start the service
  • PORT: "{{.Service.Port}}" — templated value from NDL’s service registry (no hardcoding)
  • service.port: 3000 — the port the service listens on (NDL uses this for health checks and routing)

Deploy and Access

# From the hello-node directory
ndl start
ndl apply -f manifest.yaml
ndl status

# Hit the service by name (no localhost:3000)
curl http://hello-node.default.ndl.test:18400

Output:

Hello Node from NDL!

Notice: you’re not calling localhost:3000. You’re calling hello-node.default.ndl.test:18400—a DNS name that goes through NDL’s gateway.

The Deno Version

Now let’s do the same thing with Deno.

Here’s the server code:

const port = parseInt(Deno.env.get("PORT") || "8000");

Deno.serve({ port }, () => {
  return new Response("Hello Deno from NDL!\n", {
    headers: { "content-type": "text/plain" },
  });
});

console.log(`Server running at http://localhost:${port}/`);

Again, nothing unusual—just a Deno HTTP server reading PORT from the environment.

The Manifest

Here’s the NDL manifest:

# Simple Deno Hello World - NDL Demo
#
apiVersion: apps.ndl.nuewframe.io/v1alpha1
kind: Deployment
metadata:
  name: hello-deno
  namespace: default
spec:
  source:
    workingDirectory: ".."
    command: "deno run --allow-net --allow-env server.ts"
    environment:
      PORT: "{{.Service.Port}}"
  service:
    port: 8000

The only difference: command: "deno run --allow-net --allow-env server.ts"

Everything else—the structure, the templating, the service definition—is identical.

Deploy and Access

# From the hello-deno directory
ndl start
ndl apply -f .ndl/manifest.yaml
ndl status

# Hit the service by name
curl http://hello-deno.default.ndl.test:18400

Output:

Hello Deno from NDL!

Same workflow. Same DNS-based routing. Same gateway. Different runtime.

What This Demonstrates

These two demos prove a few core NDL capabilities:

1. DNS-Based Service Names

Instead of localhost:3000 or localhost:8000, you access services via:

  • hello-node.default.ndl.test:18400
  • hello-deno.default.ndl.test:18400

The pattern is <service>.<namespace>.ndl.test:<gateway-port>.

This means:

  • No port conflicts (every service gets its own name)
  • No manual port tracking
  • Production-like routing (gateway → service, just like Ingress → Pod)

2. Port Templating

Both manifests use PORT: "{{.Service.Port}}" instead of hardcoding values.

NDL’s templating engine injects the correct port at runtime, so you never have to maintain parallel config files or .env files across services.

3. Polyglot Support

The same inner loop works for:

  • Node.js
  • Deno
  • .NET
  • Java
  • Python
  • Go
  • Any runtime with a CLI

You change one line (the command) and NDL handles the rest.

4. Declarative Workflow

In both cases, the workflow is:

  1. ndl start — start the platform
  2. ndl apply -f manifest.yaml — declare your intent
  3. ndl status — verify it’s running
  4. curl http://<service>.<namespace>.ndl.test:18400 — access by name

No manual scripting. No startup order. No port hunting.

TLS Support

If you want to test HTTPS locally (e.g., for auth flows or cookies), NDL’s gateway listens on :18443 with a self-signed certificate:

# Node.js
curl -sk https://hello-node.default.ndl.test:18443

# Deno
curl -sk https://hello-deno.default.ndl.test:18443

This is useful for:

  • Testing OAuth redirects
  • Validating secure cookie behavior
  • Simulating production-like TLS routing

Why This Matters

These demos might look trivial, but they establish something important: the inner loop is consistent across runtimes.

When you move from a “hello world” service to a multi-service distributed system, the workflow doesn’t change:

  • You still declare intent in a manifest
  • Services still get DNS names
  • The gateway still routes traffic consistently
  • Configuration still uses templating

The goal isn’t to recreate production perfectly. It’s to bring production primitives—DNS, gateways, service discovery, centralized config—into the inner loop, so local environments are repeatable and teams spend less time re-wiring the same system.

Try It Yourself

Both demos are available in the NDL repository:

Clone the repo, run the commands, and see the same workflow work for both runtimes.

Join the waitlist for early access →

From there, you can explore:

  • Multi-service apps (like the .NET Todo demo)
  • Database integration
  • Service-to-service communication
  • Centralized configuration

The promise stays the same: production primitives, locally.