Build a simple rest service in Node.js using TypeScript and deploy to App Engine
There are some guides already on how to set this up. I’m going to do exactly the same, but only add the stuff that I find useful and up to date as of July 2020.
- Create a folder (I’m calling mine “node-ts-again”) in your workspace and browse to it via a terminal. I’m actually using Windows 7 as my PC died and I’m waiting for new laptop with Ubuntu to be delivered.
- Run
npm init -y
- First add Express.js with
npm i express
Add TypeScript to detect TypeErrors on compile time rather than runtime: npm i -D typescript
- Add the type definitions with
npm i -D @types/node @types/express
- Generate a tsconfig.json file with
npx tsc --init
- Create a folder “src” with an index.ts file that has the following content:
import express from "express";
const PORT = 8080;
const app = express();app.get( "/", ( req, res ) => {
res.send( "Hello world!" );
} );const server = app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
I’ve picked port 8080 but you can pick any port you like.
Webpack
Webpack is a JavaScript bundler that handles minifying all the files. We configure webpack to use the ts-loader, which will transpile our TypeScript into JavaScript before bundling everything.
- Install the following dependencies:
npm i -D ts-loader webpack webpack-cli webpack-node-externals
My first TypeScript project was using create-react-app with the TypeScript template. The create-react-template has webpack under the hood, but as a user you don’t notice. This allowed me to play with TypeScript without having to worry about the transpilation.
As this is Node.js and not React, we will have to configure webpack ourselves. I’ve heard many people complain about webpack being complex to set up. I admit I haven’t done any actual configuring besides just copying the one in this tutorial and splitting it up in a dev and production version. But I didn’t find that too difficult to use without fully understanding it.
- Create two webpack config files, one to use while developing and one to use in production. I named mine webpack.config.ts and webpackdev.config.ts. See the contents of these files below and copy them.
webpack.config.ts
const path = require("path");
const nodeExternals = require("webpack-node-externals");
module.exports = {
entry: ['./src/index.ts'],
resolve: {
extensions: ['.ts']
},
target: "node",
mode: "production",
externals: [nodeExternals()],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js'
},
module: {
rules: [
// all files with a `.ts` extension will be handled by `ts-loader`
{ test: /\.ts$/, loader: 'ts-loader'}
]
}
};
webpackdev.config.ts
const webpack = require("webpack");
const pathDev = require("path");
const nodeExternalsDev = require("webpack-node-externals");
module.exports = {
entry: ["webpack/hot/poll?100", "./src/index.ts"],
watch: true,
target: "node",
externals: [
nodeExternalsDev({
allowlist: ["webpack/hot/poll?100"]
})
],
module: {
rules: [
{
test: /.tsx?$/,
use: "ts-loader",
exclude: /node_modules/
}
]
},
mode: "development",
resolve: {
extensions: [".tsx", ".ts", ".js"]
},
plugins: [new webpack.HotModuleReplacementPlugin()],
output: {
path: pathDev.join(__dirname, "dist"),
filename: "index.js"
}
};
Now in the bottom of your index.ts, add this code to enable modifying your code while the server is running:
/**
* Webpack HMR Activation
*/
type ModuleId = string | number;
interface WebpackHotModule {
hot?: {
data: any;
accept(
dependencies: string[],
callback?: (updatedDependencies: ModuleId[]) => void,
): void;
accept(dependency: string, callback?: () => void): void;
accept(errHandler?: (err: Error) => void): void;
dispose(callback: (data: any) => void): void;
};
}
declare const module: WebpackHotModule;
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => server.close());
}
Finally add the following to the scripts in your package.json:
"scripts": {
"start": "node dist/index",
"webpack": "webpack --config webpackdev.config.ts",
"build": "webpack --mode production",
"test": "echo \"Error: no test specified\" && exit 1"
},
Building and running this thing
To create a production release, run npm run build
to generate transpiled .js files in your dist/ folder. You can run this build with npm start
and visit http://localhost:8080/ and you should see “Hello World!”.
Now let’s say you are developing this application and you want to change this “Hello world” into an actual application you might want to quickly test whether it works without stopping the server, re-running npm run build
and npm start
again every single time. That’s what the second webpackdev.config.ts is for.
- Stop your running server.
- Open a second terminal and run
npm run webpack
You should see no errors. - Start the server again with
npm start
- Now in the code, change “Hello world!” to “Hello Leon!” and save this file.
In the terminal that is still running npm run webpack
you should see that the change has been picked up:
Also in the dist folder, you’ll notice that it created some hot-update files:
If you refresh your browser you’ll see the code changes were applied as soon as you pressed save in your editor:
This allows you to very quickly test the changes you make in your code.
Deploying to App Engine
I consider App Engine to be the most convenient way to host web apps. Because of the amazing Logging tools, Load balancer that you get out of the box. And unlike AWS, the documentation is pretty good.
So let’s add some very slight changes to host things there. You’ll need:
- A Google Cloud billing account with a project. Sign up here (do the free trial, it won’t cost anything if you stay within the free quota). Then go to https://console.cloud.google.com and click App Engine. Create a new project there. To get familiar with it, follow the interactive App Engine tutorial on the Getting Started page.
- Download the Google Cloud SDK and configure it to use your Google account and project id.
- In the node-ts-again folder we just build our Node.js backend, add an app.yaml file with the following content:
runtime: nodejs12
service: default
env: standard
instance_class: F1
handlers:
- url: '.*'
script: auto
secure: always
automatic_scaling:
min_idle_instances: automatic
max_idle_instances: automatic
min_pending_latency: automatic
max_pending_latency: automatic
- Add a .gcloudignore file to avoid uploading files that are never used on production, mostly the package.json and content in the dist folder is used on production:
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
.gcloudignore
# Node.js dependencies:
node_modules/
# Don't upload the src files as they are compiled to javascript anyway
src/
tsconfig.json
webpack.config.ts
webpackdev.config.ts
- Add one line to your index.ts to make sure you use whatever port App Engine will tell you to on production only:
import express from "express";
const PORT = process.env.PORT || 8080;
const app = express();
app.get( "/", ( req, res ) => {
res.send( "Hello Leon!" );
} );
const server = app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});/**
* Webpack HMR Activation
*/
type ModuleId = string | number;
interface WebpackHotModule {
hot?: {
data: any;
accept(
dependencies: string[],
callback?: (updatedDependencies: ModuleId[]) => void,
): void;
accept(dependency: string, callback?: () => void): void;
accept(errHandler?: (err: Error) => void): void;
dispose(callback: (data: any) => void): void;
};
}
declare const module: WebpackHotModule;
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => server.close());
}
- Deploy to production with
gcloud app deploy --version=3
Now your app is available online over https:
The code can be found here.
What’s next?
The next blog post explains how to consume and produce JSON.