How to set up an Express.js API using Webpack and TypeScript.

Photo from https://unsplash.com/photos/95YRwf6CNw8

Prerequisites.

Here is the link to the repo with the finished setup in case you get stuck.

Setting up the App.

  1. Open your terminal and create a new folder for your application. I will use the name express-app for this tutorial, but you can use any name.
mkdir express-app
cd express-app

2. Create a package.json file in the folder above by running yarn init -y. I used the -y flag to accept the default yarn options, but you can .

3. Create a src/ folder and inside that, create an index.ts file. This is the entry file of the application.

4. Create a webpack.config.js file in the express-app folder and add the following config.

const path = require('path');
const {
NODE_ENV = 'production',
} = process.env;
module.exports = {
entry: './src/index.ts',
mode: NODE_ENV,
target: 'node',
output: {
path: path.resolve(__dirname, 'build'),
filename: 'index.js'
},
resolve: {
extensions: ['.ts', '.js'],
}
}

5. Add a build script in the package.js file.

...
"scripts": {
"build": "webpack"
}

Run theyarn build command in the terminal as a smoke test for the setup.

6. Install express and it’s type definition files (@types/express) using yarn add express @types/expressand create a simple express app in src/index.ts as shown below. Type definition files enable us to enjoy the benefits of type checking.

import * as express from 'express';
import { Request, Response } from 'express';
const app = express();
const {
PORT = 3000,
} = process.env;
app.get('/', (req: Request, res: Response) => {
res.send({
message: 'hello world',
});
});
app.listen(PORT, () => {
console.log('server started at http://localhost:'+PORT);
});

The file above cannot be built by webpack out of the box. We need to add ts-loader to enable webpack to build it successfully.

7. Run yarn add ts-loader typescript --dev. The package ts-loader depends on thetypescript package. These are both development dependencies.

8. Add a typescript config file — tsconfig.json in the project root folder and add the following.

{
"compilerOptions": {
"sourceMap": true
}
}

9. Add the following webpack config to the webpack.config.js file.

...
module.exports = {
...
module: {
rules: [
{
test: /\.ts$/,
use: [
'ts-loader',
]
}
]
}
}

10. Run yarn build to test the build. This yields an error that says “Cannot find name ‘process’”. We can fix this by adding type definition files for node.js using yarn add @types/node --dev.

11. Optimization. If you open build/index.js , the first thing you notice is that the file is too large. That’s because some dependencies — packages in the node_modules folder are being added in the bundle. This makes the build process slow.

To fix this, we shall use webpack’s externals config option. This allows us to exclude some packages from the bundle.

Run yarn add webpack-node-externals --dev and add the following config to webpack.config.js

const nodeExternals = require('webpack-node-externals');
module.exports = {
...
externals: [ nodeExternals() ]
}

Run yarn build . The build/index.js file should be much smaller in size and the build time should be significantly reduced.

12. Run the application using node build/node.js . And navigate to http://localhost:3000 in your web browser.

13. Improvement. If you are developing an app with this setup, you may want to have a setup where the app automatically reloads on save. Let’s leverage webpack’s watch config option as shown below.

module.exports = {
...
watch: NODE_ENV === 'development'
}

Add a start:dev script in package.json.

...
"scripts": {
...
"start:dev": "NODE_ENV=development webpack",
}

Run yarn start:dev . With NODE_ENV set to developement , webpack is watching our app files and rebuilding on saving.

Next, we need to run the app whenever the build/index.js file changes. Let’s leverage nodemon and webpack-shell-plugin. The webpack-shell-plugin will allow us to run start the app using nodemon after webpack has finished building.

Install the packages above using yarn add --dev nodemon webpack-shell-plugin.

Add a run:dev script in package.json.

...
"scripts": {
...
"run:dev": "NODE_ENV=development nodemon build/index.js"
}

Add the following in webpack.config.js.

...
const WebpackShellPlugin = require('webpack-shell-plugin');
module.exports = {
...
plugins: [
new WebpackShellPlugin({
onBuildEnd: ['yarn run:dev']
})
]
}

I used the onBuildEnd option of the webpack-shell-plugin because it only runs the specified command(s) after the first build. On subsequent builds, nodemon will automatically restart the app.

Run yarn start:dev to run the app in development mode.

Setting up unit testing

For testing, we shall use jest as our test runner and supertest for testing the express app. Jest great support for typescript with a little configuration.

  1. Install jest, @types/jest, ts-jest and supertest as development dependencies using yarn add jest supertest @types/jest ts-jest --dev.
  2. Add some configuration for jest. Create a jest.config.js file in the same directory as the package.json with the following content.
module.exports = {
testEnvironment: 'node',
transform: {
"^.+\\.ts$": "ts-jest"
},
};

The configuration above tells jest to transform typescript files to js before running the tests.

3. Create a tests folder on the same level as the src/ folder. This will contain tests and its contents will mirror the contents of src/.

4. Create a file with the name index.test.ts in the tests/ folder with the following contents. All test file names shall end in .test.ts.

import app from '../src/index';
import * as supertest from 'supertest';
describe('app', () => {
let request;
  beforeEach(() => {
request = supertest(app);
});
  it('should return a successful response for GET /', done => {
request.get('/')
.expect(200, done);
});
});

5. Let’s modify src/index.ts to export the app variable as a default export and only start the server if the file is being executed as opposed to being imported.

...
if (require.main === module) { // true if file is executed
app.listen(PORT, () => {
console.log('server started at http://localhost:'+PORT);
});
}
export default app;

6. Add a “test” script in package.json.

...
"scripts": {
...
"test": "jest"
}

7. Run yarn test to run the unit test above. This being jest you can pass some flags too such as yarn test --watch to watch the files, yarn test --coverage to let jest collect test coverage.

Final thoughts.

Running yarn start:dev yields a deprecation warning. This is because webpack-shell-plugin is using webpack’s old plugin API. You can follow up about this on this issue.