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:
-
package.jsonwith apostinstallscript that runstsc, andtypescriptindevDependencies:{ "scripts": { "postinstall": "tsc" }, "devDependencies": { "typescript": "^5.6.0", "@types/node": "^20.0.0" } } -
tsconfig.jsonthat reads fromsrc/and writes todist/:{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "rootDir": "src", "outDir": "dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src"] } -
Your code in
src/— as many files and subfolders as you like. -
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 install → postinstall 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 indist/— they're regenerated on the next start and your changes vanish. Putdist/in.gitignoreso 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 aftertscinpostinstall) sodist/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 whilepostinstallruns. The app may still start on the previousdist/build, hiding it — fix the code insrc/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 ofsrc/, or you forgot to restart. - Main file "not found" — it must point at the compiled file,
dist/index.js, notsrc/index.ts.
Everything about the language itself — config options, types, generics — is standard TypeScript; the official handbook at typescriptlang.org takes it from here.