TypeScript projects on Falix

The compile-on-install pattern that runs any TypeScript project on the Node.js app — web servers, APIs, workers, not just bots — plus the honest limits: path aliases and watch mode.

The Node.js application runs JavaScript, and a bare .ts file won't start on it. The fix is a small, reliable pattern: compile TypeScript to JavaScript on every install, then run the JavaScript. The TypeScript Discord bot guide shows it for a bot; this guide generalises it to any TypeScript project — an Express API, a worker, a CLI — and is honest about the two things that don't carry over from your local setup.

At a glance
You need A Falix server running the Node.js application
Plan Any
Time Twenty minutes
Good to already know Node.js on Falix (auto npm install, the Main file)

The recipe, in four pieces

Every TypeScript project on the Node app comes down to the same four things:

  1. package.json with a postinstall script that runs tsc, and typescript in devDependencies:

    {
      "scripts": { "postinstall": "tsc" },
      "devDependencies": { "typescript": "^5.6.0", "@types/node": "^20.0.0" }
    }
  2. tsconfig.json that reads from src/ and writes to dist/:

    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "commonjs",
        "rootDir": "src",
        "outDir": "dist",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true
      },
      "include": ["src"]
    }
  3. Your code in src/ — as many files and subfolders as you like.

  4. The Main file variable set to dist/index.js (Settings page). That's the compiled entry point.

Why it works: starting is building

The Node app runs npm install on every start whenever a package.json is present. npm fires the postinstall script right after installing, so every start goes:

npm installpostinstall runs tsc → fresh JavaScript appears in dist/ → Node runs dist/index.js.

You never compile by hand. Edit src/, restart, and the recompile happens automatically. The compiler mirrors your src/ folder structure into dist/, so src/index.ts and src/util/greet.ts become dist/index.js and dist/util/greet.js — subfolders and all.

Here's a complete non-bot example. src/index.ts:

import { createServer } from 'node:http';
import { greet } from './util/greet';

const port = Number(process.env.SERVER_PORT) || 8080;
createServer((_req, res) => res.end(greet('Falix')))
  .listen(port, '0.0.0.0', () => console.log(`Listening on port ${port}`));

and src/util/greet.ts:

export function greet(name: string): string {
  return `Hello, ${name}, from compiled TypeScript`;
}

Set the Main file to dist/index.js, start, and the console prints Listening on port … — the same recipe serving a web app instead of a bot. It reads SERVER_PORT and binds 0.0.0.0 like any web app.

⚠️ Heads up: src/ is yours; dist/ belongs to the compiler. Never edit files in dist/ — they're regenerated on the next start and your changes vanish. Put dist/ in .gitignore so you commit source, not build output.

Honest limit 1: path aliases don't resolve at runtime

It's tempting to set up tidy imports like import { x } from '@util/thing' with paths in tsconfig.json:

"baseUrl": "src",
"paths": { "@util/*": ["util/*"] }

This compiles cleanly — the type-checker understands the alias. But tsc does not rewrite the alias in the emitted JavaScript: dist/index.js still contains require("@util/greet"), and Node has no idea what @util is. The result at runtime:

Error: Cannot find module '@util/greet'

The paths setting is a type-checking convenience only; it is not a runtime module resolver. Your options:

  • Simplest — use relative imports (./util/greet). They compile and run, no extra tooling.
  • If you really want aliases, add a build tool that rewrites them (for example tsc-alias, run right after tsc in postinstall) so dist/ ends up with real relative paths.

For most projects, relative imports are the right call — one less thing to break.

Honest limit 2: watch mode doesn't apply

Locally you probably run tsc --watch, nodemon, or ts-node-dev so saving a file recompiles and restarts instantly. None of that fits the Falix model, and you shouldn't point the Main file at a watcher:

  • The server runs your start command once. There's no file-watcher waiting for edits, and edits you make in the File Manager don't hot-reload.
  • Recompilation happens the moment you press Restart — that's your "save" step. The loop is edit src/ → Restart → read the console, exactly like plain Node.

So skip the watch tooling for the deployed server; it's a local-development convenience, not a runtime one.

The simpler alternative: Bun

If the build step feels like overhead, the Bun application runs .ts files natively — no tsc, no dist/, no postinstall. Point the Main file straight at src/index.ts and it runs. If you're starting fresh and don't need Node specifically, it's the lighter path.

Troubleshooting

  • error TS… during install — a compile error printed while postinstall runs. The app may still start on the previous dist/ build, hiding it — fix the code in src/ and restart until the install is clean.
  • Cannot find module '@…' — a path alias reaching runtime (see limit 1). Switch to a relative import or add an alias-rewriting build step.
  • Edits do nothing — you edited dist/ (regenerated every start) instead of src/, or you forgot to restart.
  • Main file "not found" — it must point at the compiled file, dist/index.js, not src/index.ts.

Everything about the language itself — config options, types, generics — is standard TypeScript; the official handbook at typescriptlang.org takes it from here.


Next steps

Was this guide helpful?