# Convert a Node.js App from JavaScript to TypeScript

Yes. An agent converts your Node.js app to strict TypeScript one directory at a time in a cloud sandbox and opens a pull request with the tsc output.

[Run this on Rush](https://cloud.getrush.ai/run/convert-javascript-to-typescript/nodejs/), in a Project that has your repository.

## What the agent does

The agent clones the repository into its own sandbox and maps the JavaScript before it renames anything. In an Express app like OWASP/NodeGoat, `package.json` has `"main": "server.js"`, `"start": "node server.js"`, Express `^4.13.4`, the `mongodb` driver at `^2.1.18`, `express-session`, `swig` templates through `consolidate`, and no TypeScript or `@types` package anywhere. The code is 50 `.js` files outside `node_modules`: `server.js`, `config/config.js` and its `config/env/*.js`, nine route handlers in `app/routes/`, seven data access objects in `app/data/`, `artifacts/db-reset.js`, `Gruntfile.js`, and the tests under `test/`. The modules are CommonJS (`module.exports = { UserDAO }`, `require("./session")`), and the DAOs are constructor functions that guard against a missing `new` (`if (false === (this instanceof AllocationsDAO))`).

It then adds the toolchain: `typescript` and the `@types` packages for Express, express-session, body-parser and Node, matched to the major versions `package-lock.json` actually installs, and a `tsconfig.json` with `"strict": true`, `"module": "commonjs"` and an `outDir` so the compiled app still starts with `node`. It reads `.github/workflows/e2e-test.yml` first, because that workflow runs Node `10.x`, `12.x` and `14.x` and the `Dockerfile` builds on `node:12-alpine`; the TypeScript release it picks has to run on those, or the PR has to say which of them it drops and why. `start`, the `Dockerfile` and `docker-compose.yml` change to build first and run the compiled `server.js`.

The conversion goes one directory at a time, leaves before roots: `config/` first, then `app/data/`, then `app/routes/`, then `server.js`. For each directory the agent renames the files to `.ts`, turns `require` into `import` where the types need it, gives each DAO a real interface for the documents it reads and writes (an allocation is `userId`, `stocks`, `funds` and `bonds`), types each handler as Express's `RequestHandler`, and declares the fields `req.session` carries, such as `userId`. It runs `npx tsc --noEmit` after each directory and fixes every error before the next one, so the history reads as a series of small commits that each compile.

NodeGoat is insecure on purpose, and a type conversion is not the place to change that. `app/routes/contributions.js` parses `preTax`, `afterTax` and `roth` with `eval(req.body.preTax)`, and the commented-out `helmet` and `csurf` lines in `server.js` are lessons, not oversights. The prompt says to keep behavior, so the agent types those lines as they are instead of rewriting them, and for your own app the same rule keeps a conversion PR from hiding a logic change. Last, it starts MongoDB in the sandbox the way the e2e workflow does, seeds it with `artifacts/db-reset.js`, runs the compiled app with `NODE_ENV=test`, and runs the Cypress suite in `test/e2e/integration/` with `npm run test:ci`.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Convert this Node.js codebase from JavaScript to TypeScript with strict mode. First list every .js file, the module style, the Node versions in CI and the Dockerfile, and the test commands. Add typescript, the @types packages for the dependencies at the versions the lockfile installs, and a tsconfig.json with strict true and an outDir; update the start script, Dockerfile and docker-compose.yml to build and run the compiled output. Convert one directory at a time, config before data access before routes before the entry point, with real interfaces for documents, request handlers and session fields instead of any, and run npx tsc --noEmit after each directory until it is clean. Do not change behavior: type existing code as it is, including code that looks unsafe, and list anything you think should change separately. Start the database in the sandbox, seed it, start the compiled app and run the existing test suite. Open a pull request with the final tsc output, the count of any and @ts-expect-error left with the reason for each, and the test results.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch: a new `tsconfig.json`, `typescript` and the `@types` packages in `devDependencies`, every converted file renamed from `.js` to `.ts` so the diff shows each one as a rename with edits, and the build changes in `package.json`, the `Dockerfile` and `docker-compose.yml`. The description carries the last `npx tsc --noEmit` output, which should be empty, and a list of every `any` and `@ts-expect-error` left, each with its file, line and the reason it could not be typed yet, such as a driver whose old API has no maintained types.

The proof is in the session's transcript, which keeps every command and its output: each `tsc` run between directories, the MongoDB start, the seed, the app start and the Cypress run. If the repository runs its own workflows on pull requests, as NodeGoat's `e2e-test.yml` and `lint.yml` do on every push and pull request, those checks run on the PR as well, and a matrix row the new TypeScript cannot run on shows up there as a failure, not a surprise after merge.

Before merging, a reviewer checks three things: that `strict` is really on in `tsconfig.json` and not loosened per file, that the interfaces match what the database stores rather than what the code wishes it stored, and that the list of things the agent thinks should change is a list, not changes hidden in the diff. A reviewer on a large codebase can ask for the PR to be split by directory with a follow-up message to the same session.

## Run it on every directory still in JavaScript

A large codebase converts better as several pull requests than as one. Start one run per directory, `app/data/` in one and `app/routes/` in the next once the first has merged, each a `POST /o/{org}/sessions` with the repository in `resources` and the directory named in `input`. Send an `Idempotency-Key` per directory so a retried request returns the run it already started.

A webhook on the API key posts `run.completed` or `run.failed` when each turn finishes, with the `session_url` to read the result and the PR. A key runs up to its `max_concurrent` sessions at once (4 by default), so independent packages in a monorepo can convert in parallel, each in its own sandbox and on its own branch.

## Why run it on Rush?

A conversion is long, mechanical work that needs the app to actually run at the end: a database, a seed, the compiled server and an end-to-end suite. On a laptop that ties up the terminal and the local MongoDB for the length of it. In a Rush sandbox the agent installs the dependencies, starts the database and the browser the Cypress specs drive, and runs `tsc` as many times as it takes, while the sandbox parks with its disk and memory when the turn ends and a follow-up wakes it where it stopped.

The result carries its evidence. Instead of a PR that says it compiles, the transcript shows each `tsc` run directory by directory, the error count falling to zero, and the test run against the compiled output. A reviewer can read exactly where an `any` was kept and why, and send a follow-up such as "type the session store too" to the same session instead of starting over.

## Frequently asked questions

### Will it use any to make the compiler pass?

The prompt asks for real types and a count of every `any` and `@ts-expect-error` left, each with a reason, in the pull request. With `strict` on, an implicit `any` is a compile error, so each one that stays is written on purpose and listed. A reviewer can send a follow-up to remove specific ones.

### Does it change how the app runs in production?

The start script, the `Dockerfile` and `docker-compose.yml` change to compile first and run the output from `outDir`, and those changes are in the diff. The app's behavior is meant to stay the same; the prompt tells the agent to type existing code as it is and list anything it would change separately.

### What if my CI runs on an old Node version?

The agent reads the Node versions in the CI workflow and the `Dockerfile` before it adds TypeScript. If the TypeScript release it needs cannot run on one of them, the pull request says which version and why, and your workflow's own run on the PR shows it.

### Can it convert the tests too?

It can, as part of the same run or a later one. For a project like NodeGoat, whose Cypress specs and Gruntfile are separate from the app code, keeping the tests in JavaScript in the first PR means they check the converted app without being converted themselves.

### Does it switch the project to ES modules?

Not unless you ask. It keeps `"module": "commonjs"` so the compiled output loads the way the JavaScript did. Moving to ES modules changes how every import resolves at runtime and is better as its own pull request.
