DEV Community

Jeremy Wells
Jeremy Wells

Posted on • Updated on

A Modern MEAN-stack with Angular and Typescript

Construction Site

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
Enter fullscreen mode Exit fullscreen mode

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 the node_modules packages. Since Angular comes with a ton of dependencies, we'll do our npm 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add these values:

{
    "compilerOptions": {
      "baseUrl": "../",
      "module": "CommonJS",
      "resolveJsonModule": false,
      "esModuleInterop": false,
      "target": "ESNext",
      "outDir": "../dist/api",
      "sourceMap": true,
      "types": [
        "node"
      ],
      "experimentalDecorators": true,
      "emitDecoratorMetadata": true
    }
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now, with the Typescript compiler, we'll turn app.ts into Javascript output:

$ tsc -p ./server/tsconfig.api.json
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In another terminal:

$ curl http://localhost:3000/api/Artie
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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\""
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In app.ts, import the libraries:

import * as livereload from 'livereload';
import * as connectLivereload from 'connect-livereload';
Enter fullscreen mode Exit fullscreen mode

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);
      });
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And paste the following:

declare module 'livereload';
declare module 'connect-livereload';
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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.

Chrome result

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)