I wanted to provide a howto on this as I couldn't find any complete information. I thought that this seemed most appropriate in Stackoverflow documentation. However it has been sunsetted *-) - Sunsetting Documentation (*).
Instead I will write this as a StackOverflow Q&A.
How to deploy a Typescript, NodeJS and Express app to Heroku
I created a project (https://gitlab.com/OehmSmith_Examples/herokumovies) that includes a README describing what needs to be done and I will reproduce that here. As a good StackOverflow practice I will also provide a copy of all the code at the bottom of this post.
This tutorial will work from https://amenallah.com/node-js-typescript-jest-express-starter/ as the base app. I have no affiliation with that site or the author. I chose it as it is simple and works. It is also an example of good OO Typescript code.
Most tutorials or even the official documentation in https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html say to install typescript globally:
> npm install -g typescript
Heroku doesn't have a global install of typescript so it needs to be kept locally. The example project does just this:
> npm i -D nodemon rimraf typescript ts-node ts-jest jest @types/jest @types/node
In the case you have pinned your @types/node
at an older version you will see something like this error:
~/AppData/Roaming/nvm/v11.15.0/node_modules/typescript/lib/lib.es2015.iterable.d.ts:41:6 - error TS2300: Duplicate identifier 'IteratorResult'.
41 type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
~~~~~~~~~~~~~~
node_modules/@types/node/index.d.ts:170:11
170 interface IteratorResult<T> { }
~~~~~~~~~~~~~~
'IteratorResult' was also declared here.
node_modules/@types/node/index.d.ts:170:11 - error TS2300: Duplicate identifier 'IteratorResult'.
170 interface IteratorResult<T> { }
~~~~~~~~~~~~~~
~/AppData/Roaming/nvm/v11.15.0/node_modules/typescript/lib/lib.es2015.iterable.d.ts:41:6
41 type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
~~~~~~~~~~~~~~
'IteratorResult' was also declared here.
Found 2 errors.
From TypeScript: Duplicate identifier 'IteratorResult'. And as per that you need to update your version of @types/node
. This was a problem I struck as I was working with older code and wanted to include this discussion of it.
Change the index.ts
to be the following instead since the original code hard-coded port 5000:
app.listen(process.env.PORT, () => {
console.log(`server started on port ${process.env.PORT}`)
});
To allow for this I have added the PORT to the npm scripts
, including adding a start:dev
so you can run it like Heroku does from the compiled typescript.
"start:dev": "PORT=5000 node dist/index.js",
"dev": "PORT=5000 nodemon --exec ts-node src/index.ts --watch src",
Or it can be set in a .env file:
PORT=5000
Heroku will NOT install dev dependencies (neither will any other cloud provider). Therefore you need to move some dependencies to the main block. For example, a NestJS
application has these as Dev Dependencies and they need to be moved:
@nestjs/cli
I added this constructor to the MoviesApi.ts
:
constructor() {
// setup some dummy data
movies.push({
name: 'Pirates of the caribbean',
rating: 8.5
})
movies.push({
name: 'Star Wars: A new hope',
rating: 8.7
})
}
Now deploy to Heroku
In your terminal:
heroku login
heroku create moviesheroku // this needs to be unique
You may need to add the returned git url as a remote (check with git remote -v
):
git remote add heroku <git url>
Lookup or search for buildpacks with (the next step already specifies those I use):
Search with:
heroku buildpacks:search typescript
Add buildpacks:
heroku buildpacks:add zidizei/typescript
heroku buildpacks:add heroku/nodejs
Confirm buildpacks:
heroku buildpacks
Commit to your local repository
git init // if not already done
git add --all
git ci -m "Initial commit. Test project all setup and should be ready to 'serve' but not yet ready to deploy to heroku"
Run with
npm dev # OR
npm run start:dev # It depends on your npm scripts
Test with postman or similar or run this from the command-line:
curl http://localhost:5000/movies
Test that it transpiles with npm run build
Update the npm scripts so that after installation (npm install
) on Heroku, it will build it before attempting to npm run start
"postinstall": "npm run build" # Depends on your npm scripts
Commit to local repository:
git add --all
git ci -m "Now it should deploy, build and run on heroku"
Deploy to heroku. It should build and start up.
git push heroku master
Test (assuming the app you heroku create
d is moviesheroku
- adjust accordingly)
curl https://moviesheroku.herokuapp.com/movies
I haven't specified a Procfile
telling Heroku anything about the app. Fortunately it creates its own defaults by determining that this is a node + npm
app. However you can explicitly define this and will need to perform this action if you have multiple apps or similar. You could add a Procfile to contain (this is the default):
web: npm start
Heroku also defaults to using one of the most recent versions of these. You could explicitly set the versions at the top-level in the package.json
file like:
"engines": {
"node": "10.x",
"npm": "6.x"
},
Although if you don't specify a npm
version then Heroku will use a sensible default for the version of node.
I had this going in only a couple hours. The main issues I needed to work out is that typescript has to be local, not global. And the buildpacks. The PORT is an issue also though every cloud provider requires the use of process.env.PORT
so this was obvious to me.
Azure was a nightmare and took days, but that was mainly because the workplace I was at insisted on using Windows servers. Long story and I won't go in to it.
AWS was so convoluted. I didn't get the instance I had working after trying for a day. however I do need to try again. The app I was trying used the https://tsed.io/ library. Simple Node / Typescript / Express apps should work out quite easily.
(*) - the sunsetting of documentation was a little bit surprising though given it happened over 2 years ago I guess it wasn't something I used. And I always thought that the Q&A was the easiest place for documentation.
.gitignore
node_modules
dist
coverage
.jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
};
package.json
{
"name": "movies",
"version": "1.0.0",
"description": "Example from https://amenallah.com/node-js-typescript-jest-express-starter/ but then modify and / or show steps for how to deploy this Typescript NodeJS Express RESTful app to Heroku.",
"main": "index.js",
"scripts": {
"build": "rimraf dist && tsc",
"postinstall": "npm run build",
"start": "node dist/index.js",
"start:dev": "PORT=5000 node dist/index.js",
"dev": "PORT=5000 nodemon --exec ts-node src/index.ts --watch src",
"test": "jest --watch",
"coverage": "jest --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.2",
"@types/jest": "^24.0.25",
"@types/node": "^13.1.2",
"jest": "^24.9.0",
"nodemon": "^2.0.2",
"rimraf": "^3.0.0",
"ts-jest": "^24.2.0",
"ts-node": "^8.5.4",
"typescript": "^3.7.4"
},
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "dist",
"sourceMap": false,
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"*": [
"node_modules/",
],
"typings/*": [
"src/typings/*"
]
},
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/test/**/*.spec.ts"
]
}
src/api/MoviesApi.ts
import IResource from "typings/IResource";
let movies: object[] = []
export default class MoviesApi implements IResource {
constructor() {
// setup some dummy data
movies.push({
name: 'Pirates of the caribbean',
rating: 8.5
})
movies.push({
name: 'Star Wars: A new hope',
rating: 8.7
})
}
create(data: any): any {
movies.push(data)
return data
}
findMany(): any[] {
return movies;
}
}
src/test/api/Movies.spec.ts
import IResource from '../typings/IResource'
import MoviesApi from '../api/MoviesApi'
const moviesApi: IResource = new MoviesApi()
describe('Movies API', () => {
it('should create a new movie', () => {
const movieData: object = {
name: 'Pirates of the caribbean',
rating: 8.5
};
const movie: object = moviesApi.create(movieData);
expect(movie).toEqual(movieData)
})
});
src/typings/IResource/index.d.ts
export default interface IResource {
create(data: any): any
findMany(): any[]
}
src/index.ts
import * as express from 'express'
import * as bodyParser from 'body-parser'
import MoviesApi from './api/MoviesApi'
const app = express();
const moviesApi = new MoviesApi();
app.use(bodyParser.json());
app.post('/movies', (req: express.Request, res: express.Response) => {
res.json(moviesApi.create(req.body))
});
app.get('/movies', (req: express.Request, res: express.Response) => {
res.json(moviesApi.findMany())
});
app.listen(process.env.PORT, () => {
console.log(`server started on port ${process.env.PORT}`)
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With