Photo by Tolu Olubode on Unsplash
Introduction
One of the advantages I love about using Angular is that the framework is truly "batteries included". From the application architecture, to configuration, to third-party libraries, to testing setup, to extra compilation tools, it's a set of really smart decisions that help get a fully featured browser application running quickly. For tasks like setting up scripts, compiling Typescript, CSS preprocessing, Webpack, and testing, the Angular CLI can save lots of tedious configuration.
Likewise, for independent projects, Node.js and Express can be great choices as they tend to be easy to deploy to a variety of platforms.
Combined with MongoDb for data persistence, these tools have long been known as the MEAN Stack (with AngularJS being the original A in MEAN), with "Javascript everywhere" being the unifying idea.
Over a few months of trying out different Angular-Express-With-Typescript setups, I've come up with a way to set up these kinds of projects using the Angular CLI as the starting point. After creating an Angular-based workspace, I can add an Express.js application written in Typescript, and configure the two to transpile Typescript files to a single deployable Node.js web app. With this set up, we can also use Nodemon and Concurrently to create a convenient dev workflow similar to what the Angular CLI provides with the ng serve --open
command.
This article will be the first in a two-part series. In part one, we'll go through setting up and configuring Angular and Express. Then we'll use Nodemon, Concurrently, and live-server to handle compiling and refreshing the application when changes are made to the code base.
In another tutorial, I'll show how we can use Docker to serve the development database, a setup that's a little more flexible and convenient than running MongoDb locally (although that's perfectly fine too).
Prerequisites
This tutorial will assume at least some familiarity with Angular and the Angular CLI, Typescript and its CLI, and Express.
The following tools should be installed before starting (the links are to their respective "Getting Started" pages).
- Node.js - I'm using version 14.15.1 as I write this.
- Angular CLI - I'm using version 11. If you're a few version behind, these steps should still work.
- Typescript - I'm using version 4.
We'll also install a couple of NPM tools globally, but I'll explain those as we come to them.
Set up an empty Angular project
The first step will be to use the Angular CLI to set up an Angular workspace so that we can take advantage of all of the framework's smart default configurations from the start.
Normally, we would use the ng new
command to create the project which would scaffold a few application components and tests to get us going. However, in our first step, we're going to set up the workspace and the application separately.
Start with this command:
$ ng new NameApp --create-application=false --new-project-root=. --skip-install=true
Notice some new flags:
-
--create-application=false
just sets up the workspace. -
--new-project-root=.
will help any configuration files (tsconfig.json
,angular.json
) find all of the locations in our project with minimal headaches. -
--skip-install=true
skips installing thenode_modules
packages. Since Angular comes with a ton of dependencies, we'll do ournpm install
all at once later on. This makes it easier to delete the entire project and start over if something doesn't turn out right.
Now we'll cd
into the project directory and create the client application:
$ cd NameApp
$ ng generate application client --skip-install=true
You'll be prompted to select if you want to add routing to the project, and your preferred CSS library.
We just created the usual, scaffolded Angular directories in a client
directory. Now we can keep separate directories for our client-side and server-side code.
Note that if you add --dry-run
or -d
to the end of both of these command, this runs the command without actually adding new files, allowing you to see how the project will be layed out first, which is very convenient for experimenting with unconventional setups.
If all the directories look correct, run npm install
to install all of the Node.js packages.
With everything installed, run ng serve --open
and test that the default application is working in a browser as expected.
Install some packages to support Express
Now that a basic browser application is working, we'll create an Express.js application that we'll write in Typescript. Everything will live in a directory called server
and we'll structure it in a similar setup to a typical Javascript Express application.
In the project's root folder, we'll install the main dependencies we'll need:
$ npm install express
$ npm install --save-dev @types/express
We've added Express as our API server, and we've added the Typescript type definitions for Express.
Next, we'll set up some files and directories for the server-side code:
$ mkdir -p server/bin
$ touch server/app.ts
$ touch server/bin/www
All of the server-side code will live in server
. The bin/www
file is a typical entry point file for an Express app, and app.ts
will be the root application file that will assemble all of the middleware for the API.
In an editor, open bin/www
and paste the following:
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app').default();
const debug = require('debug')('NameApp:server');
const http = require('http');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port, () => console.log(`Application is listening on port ${ port }`));
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
const port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
This is almost exactly what gets generated when scaffolding a typical Express application with javascript and it basically pulls in our application code to create an HTTP server in the Node.js runtime. This file will most likely stay unchanged throughout any project.
Next, open app.ts
and we'll paste in a very minimal Express setup:
import * as express from 'express';
import { Express, Request, Response } from 'express';
export default function createApp(): Express {
const app = express();
app.get('/api/:name', async (req: Request, res: Response) => {
const name = req.params.name;
const greeting = { greeting: `Hello, ${ name }` };
res.send(greeting);
});
return app;
}
Not much going on here. So far, what we can expect is that when we run the server, a GET
request to /api/Artie
will return Hello, Artie
.
Now we need to process the Typescript file and output them as Javascript that the Node.js runtime can read.
Setting up a build artifacts directory
Our intention is to output all of the Javascript code to a ./dist
directory in the root of the project. This is the directory that Angular normally compiles all of its browser code to, and it's already in the .gitignore
that Angular created. We'll modify ./dist
to instead end up with a unified deployment package once the Angular and Express code is all processed. At the end, all of our Typescript scripts will be output to the ./dist
directory as follows:
/dist
- /api
- /bin/www
- app.js
# (... everything we made with Express ...)
- /public
# (... everything we made with Angular)
With the Angular code, we only need to make a small change to the angular.json
file.
{
// ...
"projects": {
"client": {
"architect": {
"build": {
"options": {
"outputPath": "dist/public", // <--- CHANGE THIS PATH
The rest of the Angular configurations should be okay as they are.
For our server-side code, we'll add a separate tsconfig
file in the ./server
directory:
$ touch ./server/tsconfig.api.json
Add these values:
{
"compilerOptions": {
"baseUrl": "../",
"module": "CommonJS",
"resolveJsonModule": false,
"esModuleInterop": false,
"target": "ESNext",
"outDir": "../dist/api",
"sourceMap": true,
"types": [
"node"
],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
The important settings to note is that the baseUrl
value is still the root of the project so that it draws from the same node_modules
library. Then outDir
is set to the same ./dist
directory where all of the compiled output goes.
With our first app.ts
iteration written, and our configurations updated, we now need to transpile the file to Javascript and make sure that the bin/www
file can load it. We'll do the following test:
Create a ./dist
directory with an api/bin
directory at the root of the project if there isn't one there already. Then copy the www
file:
$ mkdir -p ./dist/api/bin
$ cp ./server/bin/www ./dist/api/bin
Now, with the Typescript compiler, we'll turn app.ts
into Javascript output:
$ tsc -p ./server/tsconfig.api.json
Double check that it has been created ./dist/api/app.js
.
Run the www
with Node to see if the Express.js server runs and accepts a test GET
request as expected:
$ node ./dist/api/bin/www
In another terminal:
$ curl http://localhost:3000/api/Artie
And we should see {"greeting" : "Hello, Artie"}
returned to the terminal.
Setting up scripts for unified client and server-side development
At this point you should have gotten signs of life from both the Angular and the Express apps. Now we need to combine the two so that we can serve the entire application on a single port. To do this, we'll set up Angular to build to the ./dist/public
, then set the Express server to serve the static files from that directory.
First, we'll set Express to serve static files from ./dist/public
. Here is app.ts
with those lines added:
import * as express from 'express';
import * as path from 'path'; // < -- add this
import { Express, Request, Response } from 'express';
export default function createApp(): Express {
const app = express();
const clientDir = path.join(__dirname, '../public'); // <-- add this
app.use(express.static(clientDir)); // <-- and add this
app.get('/api/:name', async (req: Request, res: Response) => {
const name = req.params.name;
const greeting = { greeting: `Hello, ${ name }` };
res.send(greeting);
});
return app;
}
Note that the location of public
is relative to the compiled app.js
when it's in the ./dist/api
directory.
Now, the following commands will 1) Build the static assets from Angular, 2) transpile the changes added to app.ts
, and 3) serve the entire application from Express as before:
$ ng build
$ tsc -p ./server/tsconfig.api.json
$ node ./dist/api/bin/www
Navigate to http://localhost:3000
and you should see the default Angular page again. Make a GET
request to http://localhost:3000/api/Oliver
and you should get {"greeting" : "Hello, Oliver"}
as the response.
Shut the server down and proceed.
With Express serving both the API and the static browser files, we'll add some scripts to the package.json
file to make all of these steps more seamless and so the server can listen to file changes.
First, we'll need to install the following npm tools globally:
-
npm install --global nodemon
- Nodemon is a development utility that will restart our API server whenever changes to the code are detected. -
npm install --global concurrently
- Concurrently is a tool that can run multiple npm processes in the same terminal, and it provides several options to deal with any of the processes failing. We'll use concurrently to watch and rebuild the client and server side code at the same time.
Now add the following scripts to package.json
:
{
// ...
"scripts": {
//...
"clean": "rm -rf ./dist/api && rm -rf ./dist/public/",
"cp:www": "mkdir -p ./dist/api/bin && cp ./server/bin/www ./dist/api/bin/",
"dev": "concurrently -k \"tsc -p ./server/tsconfig.api.json -w\" \"cd ./dist/api && nodemon -r ./bin/www --watch\" \"ng build --watch\""
}
}
Here's what they do:
-
$ npm run clean
- will clean out the directories where the compiled output goes in case we need to make a fresh start. -
$ npm run cp:www
- This copies./server/bin/www
to its proper location. -
$ npm run dev
- Using Concurrently, we compile Typescript files every time there are changes, run the files in Node and watch for changes with Nodemon, then watch for changes to the Angular files and build those accordingly.
Run each of those scripts in order and you should get the same results as above when making requests to http://localhost:3000
.
Bonus: Refresh the browser when client-side code changes
Unfortunately, one of the tradeoffs to using the above scripts instead of Angular's ng serve
is that we'd have to manually refresh the browser each time we make changes. Configuring Express with a couple of npm packages - livereload
and connect-livereload
- can accomplish this in our current setup.
Install the packages as development dependencies:
$ npm install --save-dev livereload connect-livereload
In app.ts
, import the libraries:
import * as livereload from 'livereload';
import * as connectLivereload from 'connect-livereload';
And underneath the line where the client directory is declared, paste the following:
const app = express();
const clientDir = path.join(__dirname, '../public');
// In development, refresh Angular on save just like ng serve does
let livereloadServer: any;
if (process.env.NODE_ENV !== 'production') {
livereloadServer = livereload.createServer();
livereloadServer.watch(clientDir);
app.use(connectLivereload());
livereloadServer.once('connection', () => {
setTimeout(() => livereloadServer.refresh('/'), 100);
});
}
In the code, we're creating a livereload
server and setting it to listen to changes to the client directory. Then, connect-livereload
provides middleware to the Express app that injects a bit of temporary code in our static files that makes the browser aware of any changes and refreshes accordingly.
Lastly, if your linter is giving you grief about not having type declarations for livereload
and connect-livereload
, you can add a type declarations file in the server directory:
$ touch ./server/decs.d.ts
And paste the following:
declare module 'livereload';
declare module 'connect-livereload';
I got this configuration mainly from this article which goes into much more detail about what is actually happening.
Putting it all together
As one final proof-of-life, let's get our Angular application to talk to the Express back end.
In the Angular app, open app.module.ts
and paste the all of the following:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
The only difference is that we've added the FormsModule
and the HttpClientModule
.
Next open app.component.ts
and replace the entire file with:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
template: `
<div class="app-container" style="width:20rem; margin: 2rem auto;">
<div class="form-group" >
<label for="name-input">Enter a name:</label>
<input class="form-control" id="name-input" required [(ngModel)]="nameInput">
<button class="btn btn-primary"(click)="greetMe()">Greet Me</button>
</div>
<div class="name-display">
<p *ngIf="responseDisplay && responseDisplay.length > 0">
{{ responseDisplay }}
</p>
</div>
</div>
`
})
export class AppComponent {
constructor(private http: HttpClient) { }
nameInput: string = '';
responseDisplay: string = '';
greetMe(): void {
this.http.get(`/api/${ this.nameInput }`)
.subscribe((response: any) => this.responseDisplay = response.greeting);
}
}
Optionally, you can add some basic Bootstrap so the result isn't hideous. In styles.css
, add:
/* You can add global styles to this file, and also import other style files */
@import url('https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css');
Run the entire application again with $ npm run dev
, and you should see a tiny form where you can send a name to the server, then get a greeting back.
Conclusion
The above steps should provide a good start to building out a fullstack application entirely in Typescript. By starting with Angular, we're bringing in a rich set of tools and configurations, then adding Express.js and some scripts for a convenient and automated development workflow.
In an upcoming tutorial, I'll show how to add in MongoDb (the M in MEAN) and how to use Docker to reduce some of the overhead in setting up a local database.
Top comments (0)