# Convert an Express App from JavaScript to TypeScript

Yes. An agent converts your Express app to strict TypeScript layer by layer in a sandbox, keeps the Jest suite green, and opens a pull request with the tsc output.

[Run this on Rush](https://cloud.getrush.ai/run/convert-javascript-to-typescript/express/), 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 API like hagopj13/node-express-boilerplate, `package.json` has `"main": "src/index.js"`, Express `^4.17.1`, Mongoose `^5.7.7`, Passport with `passport-jwt`, Joi `^17.3.0` for validation, Winston for logging and `pm2` in `ecosystem.config.json` for production, with `"node": ">=12.0.0"` in `engines` and a `yarn.lock`. The code is 48 `.js` files under `src/` and `tests/`: `src/app.js` and `src/index.js`, seven files in `src/config/`, two controllers, four middlewares, two Mongoose models with two plugins, three route files under `src/routes/v1/`, four services, three utils and three validation modules.

Then it reads how the tests run, because they are what keeps the conversion honest. The `test` script is `jest -i --colors --verbose --detectOpenHandles`, `jest.config.js` sets `testEnvironment` to `node` with `NODE_ENV=test`, and `tests/integration/` holds supertest suites for auth, users and docs that hit a real MongoDB through `tests/utils/setupTestDB.js`. `src/config/config.js` validates the environment with Joi and appends `-test` to `MONGODB_URL` under test. The agent starts MongoDB in the sandbox, runs the suite once in JavaScript, and keeps that result as the baseline.

The conversion goes one layer at a time, in dependency order: `config` first, then `utils`, `models` and their plugins, `services`, `middlewares`, `validations`, `controllers`, `routes`, and finally `app` and `index`. The agent adds `typescript`, `ts-jest`, `@types/express`, `@types/jest`, `@types/supertest` and the other `@types` packages at versions matching the lockfile, writes a `tsconfig.json` with `strict: true`, `outDir: dist` and `esModuleInterop`, and after each layer runs `npx tsc --noEmit` until it is clean and the Jest suite until it passes. Real interfaces replace guesses: the `User` document type from `src/models/user.model.js` with its `isPasswordMatch` method and `isEmailTaken` static, the `ApiError` class in `src/utils/ApiError.js` with its `statusCode` and `isOperational`, and typed request handlers for `catchAsync`.

It types code as it is rather than changing behavior. The `toJSON` and `paginate` plugins, the `pick` helper and the role map in `src/config/roles.js` keep their shapes; where a value is genuinely untyped, such as the Joi-validated environment, the agent writes the interface the schema implies. The `start` script, `ecosystem.config.json` and the `Dockerfile` move from `src/index.js` to the compiled `dist/index.js`, with a `build` script added. The ESLint setup, which is `eslint:recommended` through `airbnb-base` with the `jest`, `security` and `prettier` plugins in `.eslintrc.json`, gets the TypeScript parser and plugin so `yarn lint` still runs over the converted files.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Convert this Express codebase from JavaScript to TypeScript with strict mode. First list every .js file by directory, the module style, the Node version in engines and CI, the package manager from the lockfile, and the test command and what it needs to run. Start the database the tests need in the sandbox and run the existing test suite once as the baseline. Add typescript, ts-jest and the @types packages for the dependencies at versions matching the lockfile, and a tsconfig.json with strict true, esModuleInterop and an outDir; add a build script and point the start script, the process manager config and the Dockerfile at the compiled output. Convert one directory at a time in dependency order, config before utils before models before services before middlewares before routes before the entry point, with real interfaces for documents, request handlers, errors and config instead of any, and run npx tsc --noEmit and the test suite after each directory until both are clean. Update the ESLint config so lint still runs over .ts files. Do not change behavior: type existing code as it is and list anything you think should change separately. Open a pull request with the final tsc output, the test results, and the count of any and @ts-expect-error left with the reason for each.",
  "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: `tsconfig.json`, the new dev dependencies in `package.json` and `yarn.lock`, every file under `src/` and `tests/` renamed from `.js` to `.ts` so GitHub shows each as a rename with edits, and the build changes in the `start` script, `ecosystem.config.json`, `Dockerfile` and `.eslintrc.json`. New type files sit beside the code they describe, such as a `User` document interface next to the model.

The description carries the last `npx tsc --noEmit` output, which should be empty, the Jest run with the same tests passing as the JavaScript baseline, and a table of every `any` and `@ts-expect-error` left with the file, line and reason. Anything the agent thinks should change in behavior, such as a handler that swallows an error, is listed separately rather than fixed, so the diff stays a conversion.

A reviewer checks that the test count matches the baseline, that the `start` path in `ecosystem.config.json` and the `Dockerfile` points at `dist/`, and reads the `any` table to decide what to tighten later. The session read returns the repository and the pull request, and the transcript holds every `tsc` and Jest run in the order the agent made them.

## Run it on every directory still in JavaScript

A large codebase converts better in slices than in one pull request. Start one run per directory from your server, with a prompt that names the directory and stops there, and each run opens a pull request a reviewer can read in one sitting. Because every run makes its own branch in its own sandbox, the `config` and `utils` slices can run at once; `routes` waits for `controllers` to merge, and a follow-up message to a finished session continues in the same sandbox with the merged code.

With a `webhook_url` on the key, each `run.completed` event names the session to read the pull request from, so the next slice can be started when the previous one merges without anyone polling.

## Why run it on Rush?

A conversion is only safe when the tests pass after every step, and that means a MongoDB instance, a clean install and dozens of `tsc` runs. The sandbox has the database, the compiler and the test runner, and the transcript records every run in order, so the reviewer reads the baseline and the final run instead of trusting a description.

The work is long and mechanical, which is exactly what a laptop is bad at holding for an afternoon. On Rush it runs in the cloud while the developer does something else; the sandbox parks when the turn ends, and a follow-up message wakes it with the same converted tree to tighten a type or convert the next directory.

## Frequently asked questions

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

Not as a shortcut. The prompt asks for real interfaces for documents, handlers, errors and config, and for every remaining `any` or `@ts-expect-error` to be listed with its reason in the pull request. A reviewer sees the count and decides whether it is acceptable.

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

Only the entry path. The boilerplate starts with `pm2 start ecosystem.config.json`, whose script is `src/index.js`; after the conversion it is the compiled `dist/index.js` and a `build` step runs first. The Dockerfile gets the same change. Nothing in the request handling, the Mongoose models or the Joi validation changes.

### My CI runs on an old Node version. Does that matter?

The agent reads `engines` and the CI config and picks a TypeScript target that compiles for the oldest version named. The boilerplate declares `">=12.0.0"` and a Travis config on Node 12, so the pull request says which target it chose and that a newer Node would allow a higher one.

### Can it convert the tests too?

Yes. The `tests/` directory, including the supertest integration suites and the fixtures under `tests/fixtures/`, is converted with `ts-jest` in `jest.config.js`, and the suite is run after the conversion so the pull request shows the same tests passing in TypeScript.

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

No. The boilerplate uses `require` and `module.exports` throughout, and the conversion keeps CommonJS output with `esModuleInterop` on so the imports read as modern TypeScript without changing the module system. A switch to ESM is a separate change you can ask for in a follow-up.
