Let’s build a MEAN app using TypeScript for the client and server using Angular Material and Reactive programming.
Goals
- Create a simple CRUD app similar to the Tour of Heros tutorial app for Angular.
- Create the REST API using Express written in TypeScript with Mongoose for persisting data to MongoDb.
- Use Angular Material as the UI toolkit.
- Use Reactive Extensions for JavaScript, specifically, RxJS and ngrx.
Install Node and npm
To get started we will create a new project using the Node Package Manager (npm).
If you have not installed Node, which includes npm, I would suggest you use http://brew.sh/:
</code>$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew update
$ brew doctor</pre>
Then, install node using the `brew install` command:
```bash
$ brew install node
Next, create a new project using npm. I am going to call the project “mean-material-reactive”:
$ mkdir mean-material-reactive
$ cd mean-material-reactive
$ npm init
Follow the prompts in order to create a new project. At this point your project should contain a single package.json file.
Install Gulp
The next step is to install Gulp, which we will be using to automate our workflow. We’ll also install several plugins that we will need:
$ npm install gulp-cli -g
$ npm install gulp --save-dev
$ npm install typescript --save-dev
$ npm install del --save-dev
$ npm install gulp-sourcemaps --save-dev
$ npm install gulp-typescript --save-dev
$ npm install run-sequence --save-dev
$ npm install ts-node --save-dev
$ touch gulpfile.js
$ touch gulpfile.ts
Note the use of the --save-dev
flag, which instructs npm to write the dependency into our project’s package.json file.
Here is a quick rundown of the plugins that we are using:
- del - This plugin for Gulp will be used to delete all of the files in our dist directories.
- gulp-sourcemaps - for writing out the source maps for the browser.
- gulp-typescript - for compiling our TypeScript to JS.
- run-sequence - enables us to run a series of gulp tasks in a specific order.
- ts-node - enables us to execute TypeScript code in Node.
Also, note that I have created two empty files; the gulpfile.js file will use ts-node to execute the gulpfile.ts TypeScript file.
Here is what the gulpfile.js file will contain:
require('ts-node').register({
project: false,
disableWarnings: true,
});
require('./gulpfile.ts');
Let’s start building the gruntfile.ts file. We will start by created two tasks:
- clean - will delete our distributable directories.
- build:express - will build our Express HTTP server.
We will also create a default task that will use the run-sequence plugin to execute a series of Gulp tasks.
This is just the start of our gulp tasks. We will be building on this more as we get our application built.
const gulp = require('gulp'),
del = require('del'),
runSequence = require('run-sequence'),
sourceMaps = require('gulp-sourcemaps'),
tsc = require('gulp-typescript');
/**
* Remove dist directory.
*/
gulp.task('clean', (done) => {
return del(['dist'], done);
});
/**
* Copy start script.
*/
gulp.task('copy', () => {
return gulp.src('server/bin/*').pipe(gulp.dest('dist/bin'));
});
/**
* Build the server.
*/
gulp.task('build:express', () => {
const project = tsc.createProject('server/tsconfig.json');
const result = gulp
.src('server/src/**/*.ts')
.pipe(sourceMaps.init())
.pipe(project());
return result.js.pipe(sourceMaps.write()).pipe(gulp.dest('dist/server'));
});
/**
* Build the project.
*/
gulp.task('default', (done) => {
runSequence('clean', 'copy', 'build:express');
});
If you attempt to execute gulp in the project directory you should receive an error. This is because we have not yet installed Express and configured the server code via the tsconfig.json file. Let’s do that.
Install Express
Next, let’s install Express:
$ npm install express --save
Note the --save
flag.
This will save a production dependency in the package.json file.
Next, let’s set up our server folder structure:
$ mkdir server
$ cd server
$ touch tsconfig.json
Then, modify the tsconfig.json file that will contain the server’s TypeScript configuration:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true,
"typeRoots": [
"../node_modules/@types/"
]
},
"compileOnSave": true,
"exclude": [
"node_modules/*"
]
}
If you are not familiar with the TypeScript compiler options and using the tsconfig.json file, check out the documentation on tsconfig.json.
Server Start Script
Next we need to create our Express HTTP server start script:
$ mkdir src
$ mkdir bin
$ cd src/bin
$ touch www
Here is the full contents of the www file:
#!/usr/bin/env node
'use strict';
//module dependencies
var server = require('../server/server');
var debug = require('debug')('express:server');
var http = require('http');
//create http server
var httpPort = normalizePort(process.env.PORT || 8080);
var app = server.Server.bootstrap().app;
app.set('port', httpPort);
var httpServer = http.createServer(app);
//listen on provided ports
httpServer.listen(httpPort);
//add error handler
httpServer.on('error', onError);
//start listening on port
httpServer.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var 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;
}
var 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() {
var addr = httpServer.address();
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Listening on ' + bind);
}
This start script was slightly modified by an example on the Google Cloud Platform developer website, so I take not credit for it. This will create our HTTP server on the default port of 8080. You can customize this to run on the standard HTTP port 80 in production.
Next, we need to modify the permissions on the www file so that we can execute it:
$ chmod +x www
Install Middleware
The first step of using Express is to install and configure all of the middleware that our application needs. Let’s install these via npm:
$ npm install body-parser --save
$ npm install morgan --save
$ npm install errorhandler --save
Then, we need to install the TypeScript declaration files for each:
$ npm install @types/body-parser --save-dev
$ npm install @types/morgan --save-dev
$ npm install @types/errorhandler --save-dev
Here’s a quick rundown of each of these and what they do:
- body-parser - parses JSON and URL encoded data into the
body
property of theRequest
object. - morgan - logs all HTTP requests.
- errorhandler - a development-only error handler for debugging.
Server
Class
Let’s start building out the Server
class:
import * as bodyParser from "body-parser";
import * as express from "express";
import * as morgan from "morgan";
import * as path from "path";
import errorHandler = require("errorhandler");
import mongoose = require("mongoose");
/**
* The server.
*
* @class Server
*/
export class Server {
/**
* The express application.
* @type {Application}
*/
public app: express.Application;
/**
* Bootstrap the application.
* @static
*/
public static bootstrap(): Server {
return new Server();
}
/**
* @constructor
*/
constructor() {
//create expressjs application
this.app = express();
//configure application
this.config();
//add api
this.api();
}
/**
* Create REST API routes
*
* @class Server
*/
public api() {
//empty for now
}
/**
* Configure application
*
* @class Server
*/
public config() {
// morgan middleware to log HTTP requests
this.app.use(morgan("dev"));
//use json form parser middlware
this.app.use(bodyParser.json());
//use query string parser middlware
this.app.use(bodyParser.urlencoded({
extended: true
}));
// connect to mongoose
mongoose.connect("mongodb://localhost:27017/mean-material-reactive");
mongoose.connection.on("error", error => {
console.error(error);
});
//catch 404 and forward to error handler
this.app.use(function(err: any, req: express.Request, res: express.Response, next: express.NextFunction) {
err.status = 404;
next(err);
});
//error handling
this.app.use(errorHandler());
}
}
Some things to note:
- First, we import all of the middleware and dependencies that we need. Note, I am importing mongoose, but we haven’t installed it yet. We’ll get to that shortly.
- Next, I defined our
Server
class. - Then, I have a public property named
app
that will store reference to the running Express application. - The static
bootstrap()
method will instantiate a new instance of theServer
and return it. - The
constructor()
function creates a new express application, and then invokes theconfig()
andapi()
methods. - For now, the
api()
method is left empty. This is where we will build out each REST API endpoint. - The
config()
method wires up all of the middleware with our app, as well as the mongoose connection.
Install MongoDB and Mongoose
The next step is to install MongoDB. We’ll be using Homebrew to do this (we already installed Homebrew above in order to install Node).
$ brew install mongodb
$ ln -sfv /usr/local/opt/mongodb/*.plist ~/Library/LaunchAgents
The second command above will enable MongoDb to start when your system starts. This is handy for your development enviroment.
Now, install Mongoose using npm:
$ npm install mongoose --save
$ npm install @types/mongoose --save-dev
$ npm install @types/mongodb --save-dev
Note that I have also installed the TypeScript declaration files for both mongoose as well as MongoDb.
Define Interface
The next step is to define our application’s interfaces.
For this tutorial I am going to have a single Hero
interface.
First, create the server/src/interfaces directory along with a hero.ts file:
$ mkdir interfaces
$ cd interfaces
$ touch hero.ts
Here is the very simply interface for our Hero
:
export interface Hero {
name?: string;
}
Our Hero
only has a single string
property: name
.
Define Schema
The next step is to define a schema for our model. Create a new server/src/schemas directory along with a hero.ts file:
$ mkdir schemas
$ cd schemas
$ touch hero.ts
Then, define the schema in hero.ts:
import { Schema } from "mongoose";
export var heroSchema: Schema = new Schema({
createdAt: { type: Date, default: Date.now },
name: String
});
You might note that I also included an additional property named createdAt
in my schema.
This is going to be a Date
that tracks when the hero was created, as the default value is Date.now
.
Define Model
Finally, create the model that is based on our interface and schema. Create a new server/src/models directory along with the hero.ts file:
$ mkdir models
$ cd models
$ touch hero.ts
Now, let’s define our model:
import mongoose = require("mongoose");
import { Document, Model } from "mongoose";
import { Hero as HeroInterface } from "../interfaces/hero";
import { heroSchema } from "../schemas/hero";
export interface HeroModel extends HeroInterface, Document {}
export interface HeroModelStatic extends Model<HeroModel> {}
export const Hero = mongoose.model<HeroModel, HeroModelStatic>("Hero", heroSchema);
Great!
We have now defined our Hero
interface and the associated schema and model for interfacing with MongoDb using Mongoose.
Review
Ok, if you made it this far then let’s quickly review what we have accomplished so far. We have:
- Installed Node.js and npm using Homebrew.
- Installed Gulp along with several plugins to automate our workflow.
- Installed Express and the necessary middleware for our application.
- Created the www binary to execute Express on Node.js.
- Created the
Server
class that will create our Express application and wire up the middleware. - Installed MongoDb using Homebrew.
- Installed Mongoose and defined our initial
Hero
interface, schema and model.
From here we will:
- Create our the REST endpoint for our heros at: /api/hero
- Test our REST API using mocha, chai, chai-http and mocha-typescript.
- Create a new Angular application using the Angular CLI.
- Install and configure Angular Material.
- Use Sass instead of CSS.
- Install RxJs and ngrx for Reactive extensions.
REST API
The next step for our application is to define a REST API in our Express application. For this tutorial we will only be defining a single REST API endpoint for our heros. Let’s start by creating the server/src/api directory and a new ** heros.ts** file:
$ mkdir api
$cd api
$ touch heros.ts
Now, create a new HerosApi
class that will handle our CRUD operations using REST:
// express
import { NextFunction, Response, Request, Router } from "express";
// model
import { Hero } from "../models/hero";
/**
* @class HerosApi
*/
export class HerosApi {
/**
* Create the api.
* @static
*/
public static create(router: Router) {
// DELETE
router.delete("/heros/:id([0-9a-f]{24})", (req: Request, res: Response, next: NextFunction) => {
new HerosApi().delete(req, res, next);
});
// GET
router.get("/heros", (req: Request, res: Response, next: NextFunction) => {
res.status(404).send("Not found");
next(null);
});
router.get("/heros/:id([0-9a-f]{24})", (req: Request, res: Response, next: NextFunction) => {
new HerosApi().get(req, res, next);
});
// POST
router.post("/heros", (req: Request, res: Response, next: NextFunction) => {
new HerosApi().create(req, res, next);
});
// PUT
router.put("/heros/:id([0-9a-f]{24})", (req: Request, res: Response, next: NextFunction) => {
new HerosApi().update(req, res, next);
});
}
/**
* Create a new hero.
* @param req {Request} The express request object.
* @param res {Response} The express response object.
* @param next {NextFunction} The next function to continue.
*/
public create(req: Request, res: Response, next: NextFunction) {
// create hero
let hero = new Hero(req.body);
hero.save().then(hero => {
res.json(hero.toObject());
next();
}).catch(next);
}
/**
* Delete a hero.
* @param req {Request} The express request object.
* @param res {Response} The express response object.
* @param next {NextFunction} The next function to continue.
*/
public delete(req: Request, res: Response, next: NextFunction) {
// verify the id parameter exists
const PARAM_ID: string = "id";
if (req.params[PARAM_ID] === undefined) {
res.sendStatus(404);
next();
return;
}
// get id
var id: string = req.params[PARAM_ID];
// get hero
Hero.findById(id).then(hero => {
// verify hero exists
if (hero === null) {
res.sendStatus(404);
next();
return;
}
hero.remove().then(() => {
res.sendStatus(200);
next();
}).catch(next);
}).catch(next);
}
/**
* Get a hero.
* @param req {Request} The express request object.
* @param res {Response} The express response object.
* @param next {NextFunction} The next function to continue.
*/
public get(req: Request, res: Response, next: NextFunction) {
// verify the id parameter exists
const PARAM_ID: string = "id";
if (req.params[PARAM_ID] === undefined) {
res.sendStatus(404);
next();
return;
}
// get id
var id: string = req.params[PARAM_ID];
// get hero
Hero.findById(id).then(hero => {
// verify hero was found
if (hero === null) {
res.sendStatus(404);
next();
return;
}
// send json of hero object
res.json(hero.toObject());
next();
}).catch(next);
}
/**
* Update a hero.
* @param req {Request} The express request object.
* @param res {Response} The express response object.
* @param next {NextFunction} The next function to continue.
*/
public update(req: Request, res: Response, next: NextFunction) {
const PARAM_ID: string = "id";
// verify the id parameter exists
if (req.params[PARAM_ID] === undefined) {
res.sendStatus(404);
next();
return;
}
// get id
var id: string = req.params[PARAM_ID];
// get hero
Hero.findById(id).then(hero => {
// verify hero was found
if (hero === null) {
res.sendStatus(404);
next();
return;
}
// save hero
Object.assign(hero, req.body).save().then((hero: HeroModel) => {
res.json(hero.toObject());
next();
}).catch(next);
}).catch(next);
}
}
If you are unfamiliar with using Express and TypeScript, I suggest you check out my post on TypeScript 2 + Express + Node where I cover this more in depth.
Some things to note:
- We first import the necessary classes and interfaces from Express, along with our
Hero
model. - The static
create()
method requires theRouter
instance from ourServer
class and configures the routes for the /api/heros endpoint. - We implement the following verbs: DELETE, GET, POST, and PUT. Each verb associates invokes an appropriate method in the
HerosApi
class. - Each method requires the Express
Request
,Response
andNextFunction
objects. - There is no authentication or verification happening, meaning our API is open to the public. This is most likely not a good idea for prodution use. I am not including this in this tutorial, but one way to solve this is to have all of your REST API classes extend a
BaseApi
class that has anauthorize()
method that verifies your user is authenticated. You may also want to add additional logic in each REST API method for verifying that a user has permission to perform the specific action. For simplicity I am not including this here. - Using Mongoose the data is persisted to the MongoDb Heros collection.
- Note that when sending the json result to the client using the
res.json()
method I first invoke the.toObject()
method on theDocument
that is returned from mongoose. - If an exception occurs we simply use the
catch()
method of thePromise
object to invoke thenext
method. - After sending a status code or the JSON result to the client be sure to invoke
next()
.
Before we go any further we need to allow cross-origin requests. Our server (Express) and client (Angular) will be running on the same local machine, but on different ports (one on port 8080 and the other on Angular CLI’s default port of 4200), so we need to enable CORS. To do this, let’s use and install the cors middleware:
$ npm install cors --save
$ npm install @types/cors --save-dev
Then, import the cors middleware into the Server
class in server/src/server.ts:
import * as cors from 'cors';
Next, import the HerosApi
class into our Server
class.
import { HerosApi } from './api/heros';
If you recall, we left the api()
method in our Server
class empty.
Now it’s finally time to implement this and to wire up our HerosApi
:
export class Server {
// code omitted
/**
* REST API endpoints.
*/
public api() {
var router = express.Router();
// configure CORS
const corsOptions: cors.CorsOptions = {
allowedHeaders: ["Origin", "X-Requested-With", "Content-Type", "Accept", "X-Access-Token"],
credentials: true,
methods: "GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE",
origin: "http://localhost:4200",
preflightContinue: false
};
router.use(cors(corsOptions));
// root request
router.get("/", (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.json({ announcement: "Welcome to our API." });
next();
});
// create API routes
HerosApi.create(router);
// wire up the REST API
this.app.use("/api", router);
// enable CORS pre-flight
router.options("*", cors(corsOptions));
}
}
A couple of things to note:
- First, we create a new
Router
instance, which will be passed into theHerosApi.create()
static method. We will use this router for the base REST API URL: “/api”. - A
CorsOptions
object defines the options for configuring the CORS middleware. I am currently only allowing cross-origin requests from the URL ”http://localhost:4200”, which is where my Angular application will be served from. For a production instance this would likely be a different URL or set of URLs. - Next, I create a root request that will respond to GET requests to “/api”, without specifying an endpoint. This is not necessary, but is good for testing that your API is alive. This simply replies with a JSON object that has an
announcement
property. - Then, I create my API routes. In this example, there is just the single
HerosApi
class. - Then, we invoke the
use()
method on our Express application and provide the base URL of “/api”. - Finally, we enable CORS pre-flight, which is necessary when using Angular
Http
class. Note that the CORS pre-flight configuration is after we wire up our REST API.
Test REST API
We’ll be using a testing framework called Mocha, along with Chai for BSD style assertions, and the chai-http plugin. If you haven’t caught on already, I’m a huge TypeScript fan. So, we’ll also be using mocha-typescript to write our tests using TypeScript, and leveraging some ES6/ES2015 features like decorators.
To get get our testing started, let’s get things installed via npm:
$ npm install mocha --save-dev
$ npm install chai --save-dev
$ npm install chai-http --save-dev
$ npm install mocha-typescript --save-dev
$ npm install @types/mocha --save-dev
$ npm install @types/chai --save-dev
$ npm install @types/chai-http --save-dev
Let’s create a server/src/tests directory, and a heros.ts test file:
$ mkdir tests
$ cd tests
$ touch heros.ts
Let’s start building our test:
process.env.NODE_ENV = "test";
// mocha
import "mocha";
import { suite, test } from "mocha-typescript";
// mongodb
import { ObjectID } from "mongodb";
// server
import { Server } from "../server";
// model
import { Hero } from "../interfaces/hero";
import { HeroModel, HeroModelStatic } from "../models/hero";
import { heroSchema } from "../schemas/hero";
// mongoose
import mongoose = require("mongoose");
//require http server
var http = require("http");
//require chai and use should assertions
let chai = require("chai");
chai.should();
//configure chai-http
chai.use(require("chai-http"));
Here is what we are doing at the top of the hero.ts test file:
- First, we set the NODE_ENV environment variable to “test”. This is good practice, although I am not doing any runtime switching on this, it could be used for no-op code or mocking.
- Next, we import the mocha library. Then, we import the
suite
andtest
decorators from the mocha-typescript package. - Next, we import the
ObjectID
class from the mongodb package. - Next, we import our
Server
class. - Then, we import our
Hero
interface, theHeroModel
andHeroModelStatic
classes, and finally, theheroSchema
. We need these to configure our Mongoose document for testing. - Next, we import the
mongoose
library. - Next, we import the Node.js
http
server. - Then, we import
chai
and instruct chai to use should style assertions. - Finally, we are using the chai-http plugin for chai to perform HTTP response assertions.
Now that we have the imports out of the way, let’s start to define out HerosTest
class:
@suite class HerosTest {
// constants
public static BASE_URI: string = "/api/heros";
// the mongooose connection
public static connection: mongoose.Connection;
// hero model
public static Hero: HeroModelStatic;
// hero document
public static hero: HeroModel;
// the http server
public static server: any;
/**
* Before all hook.
*/
public static before() {
// connect to MongoDB
mongoose.connect("mongodb://localhost:27017/mean-material-reactive");
HerosTest.Hero = mongoose.model<HeroModel, HeroModelStatic>("Hero", heroSchema);
// create http server
let port = 8001;
let app = Server.bootstrap().app;
app.set("port", port);
HerosTest.server = http.createServer(app);
HerosTest.server.listen(port);
return HerosTest.createHero();
}
/**
* After all hook
*/
public static after() {
return HerosTest.hero.remove()
.then(() => {
return mongoose.disconnect();
});
}
/**
* Create a test hero.
*/
public static createHero(): Promise<HeroModel> {
let data: Hero = {
name: "Brian Love"
};
return new HerosTest.Hero(data).save().then(hero => {
HerosTest.hero = hero;
});
}
}
Our HerosTest
class is starting to come together.
- We defined a test suite using the
suite
decorator. - Inside our test suite I have defined several static properties and methods. Using mocha-typescript we can define static
before()
andafter()
methods that are invoked once; first,before()
all of our tests are executed, and thenafter()
all of our test have executed. This enables us to set things up, and then to tear things down. - In the static
before()
method we first establish a connection to the MongoDb server usingmongoose.connect()
. Then, I create the document model using the importedHeroModel
andHeroModelStatic
classes that we defined previously. - Next, we set up our HTTP server and start it.
- Then, I create a new hero in the
createHero()
static method. This method returns aPromise<HeroModel>
object. Note, that I return the Promise object in thebefore()
static method as well. I am returning a promise object from my tests rather than using mocha’s more traditionaldone()
callback approach. I think this is cleaner and easier to use. - In the static
after()
method I remove my test hero and disconnect from MongoDb.
Now that we have the bulk of the test setup and configured, let’s starting adding tests:
@suite class HerosTest {
// code omitted
@test public delete() {
let data: Hero = {
name: "To be deleted"
};
return new HerosTest.Hero(data).save().then(hero => {
return chai.request(HerosTest.server).del(`${HerosTest.BASE_URI}/${hero._id}`).then(response => {
response.should.have.status(200);
});
});
}
@test public get() {
return chai.request(HerosTest.server).get(`${HerosTest.BASE_URI}/${HerosTest.hero._id}`).then(response => {
response.should.have.status(200);
response.body.should.be.a("object");
response.body.should.have.property("name").eql(HerosTest.hero.name);
});
}
@test public post() {
let data: Hero = {
name: "Magneto"
};
return chai.request(HerosTest.server).post(HerosTest.BASE_URI)
.send(data)
.then(response => {
response.should.have.status(200);
response.body.should.be.a("object");
response.body.should.have.a.property("_id");
response.body.should.have.property("name").eql(data.name);
return HerosTest.Hero.findByIdAndRemove(response.body._id).exec();
});
}
@test public put() {
let data: Hero = {
name: "Superman"
}
return chai.request(HerosTest.server).put(`${HerosTest.BASE_URI}/${HerosTest.hero._id}`)
.send(data)
.then(response => {
response.should.have.status(200);
response.body.should.be.a("object");
response.body.should.have.a.property("_id");
response.body.should.have.property("name").eql(data.name);
});
}
}
Let’s review our tests:
- Note, we define a test using the
@test
decorator. - First, we have a
delete()
test. In our delete test we first create a new hero that will be deleted using the REST API. We’ll test this by ensuring that our API reponds with a 200 OK status code. - Next, we have a
get()
test. This simply get’s our hero (me, haha) that was created in thecreateHero()
static method before the tests were executed. We verify that the response has a 200 status, is an object and includes a property name with the correct value. - Next, we have a
post()
test to create a new hero. We define thedata
that we will POST to the server, and then send the request. We verify that the response is a 200, that it is an object, that it contains both the_id
andname
properties, and that thename
property is equal to the value that we sent in the data. Finally, we remove the new hero that we created and return that promise. - Finally, we have a
put()
test to update our hero. In this test we will update our hero’s name. To verify the test was successful we ensure that the reponse is a 200, that it is an object, that it contains both the_id
andname
properties, and that the value of thename
property is equal to the value that we sent in the data.
Let’s go ahead and run our tests to ensure that our REST API is working and operational.
Before we can run our tests, we need to create a new task in our gulpfile.ts. But, before we do that, let’s install the gulp-mocha package:
$ npm install gulp-mocha --save-dev
Now, let’s add a new task to the gulpfile.ts:
gulp.task('test:express', () => {
gulp.src('dist/server/tests', { read: false }).pipe(gulpMocha());
});
Now, add the test:express task to the sequence of tasks to execute in the default task:
gulp.task('default', (done) => {
runSequence('clean', 'copy', 'build:express', 'test:express');
});
At this point we are ready to run gulp in our project:
$ gulp
You should see a success message indicating that all four tests have passed. Sweet!
Install Angular
With our server setup completed, our REST API created, and our tests passing, we are now ready to get into the client side of our application.
Unfortunately, we cannot use the Angular CLI to create a new project, as we already have a project created at this point. So, we’ll create the required files for our Angular app.
First, we need to install all of the Angular production dependencies:
$ npm install @angular/animations@latest @angular/common@latest @angular/compiler@latest @angular/core@latest @angular/forms@latest @angular/http@latest @angular/platform-browser@latest @angular/platform-browser-dynamic@latest @angular/router@latest core-js@latest rxjs@latest zone.js@latest --save
Next, install the Angular development dependencies:
$ npm install @angular/cli@latest @angular/compiler-cli@latest @angular/language-service@latest @types/jasmine@latest @types/node@latest codelyzer@latest jasmine-core@latest jasmine-spec-reporter@latest karma@latest karma-chrome-launcher@latest karma-cli@latest karma-coverage-istanbul-reporter@latest karma-jasmine@latest karma-jasmine-html-reporter@latest protractor@latest ts-node@latest tslint@latest --save-dev
Create .angular-cli.json
To get started with our application, create an .angular-cli.json file in your project’s root directory:
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "mean-material-reactive"
},
"apps": [
{
"root": "./client/src",
"outDir": "./dist/client",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.scss"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json"
},
{
"project": "src/tsconfig.spec.json"
},
{
"project": "e2e/tsconfig.e2e.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "scss",
"component": {}
}
}
A few things to note:
- I updated the
root
value to point to the ./client/src folder. - Secondly, I updated the
styles
property to use Sass, so the file extension is .scss. - I also updated the
styleExt
property value to use the “scss” file extension.
Next, create the client/tsconfig.json file as follows:
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"baseUrl": "src",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"../node_modules/@types"
],
"lib": [
"es2016",
"dom"
]
}
}
Create /client
First, create the src/client directory:
$ mkdir client
$ cd client
Next, create the client/tslint.json file:
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
"static-before-instance",
"variables-before-functions"
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"typeof-compare": true,
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"no-access-missing-member": true,
"templates-use-public": true,
"invoke-injectable": true
}
}
Create /client/src
Now, create the client/src directory:
$ mkdir src
$ cd src
$ touch main.ts
Then, create our Application’s client/src/main.ts file:
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
Next, create the client/src/index.html file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>MEAN Material Reactive App</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
Next, create the client/src/polyfills.ts file:
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.
/**
* Need to import at least one locale-data with intl.
*/
// import 'intl/locale-data/jsonp/en';
Then, create the client/src/tsconfig.app.json file:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"baseUrl": "",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}
Finally, stub out the client/src/styles.scss file:
$ touch styles.scss
Create /client/src/environments
Next, create the client/src/environments directory:
$ mkdir environments
$ touch environment.prod.ts
$ touch environment.ts
Within the client/src/environments directory we created two new files, environment.prod.ts and environment.ts. Here is what the environment.prod.ts file looks like:
export const environment = {
production: true,
};
And, here is the contents of the environment.ts file:
export const environment = {
production: false,
};
Create /client/src/app
Next, create the client/src/app directory:
$ mkdir app
$ cd app
$ touch app.module.ts
And then create the client/src/app/app.module.ts file that defines the AppModule
class:
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
Next, create the client/src/app/app.component.ts file that defines the AppComponent
class:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'app';
}
Note, I am going to be using Sass instead of vanilla CSS in my application, so I am setting the stylesUrls
to use a Sass file.
Let’s create our client/src/app/app.component.html file next:
<h1>{{ title }}</h1>
And, then just stub out the client/src/app/app.component.scss file:
$ touch app.component.scss
Serve Angular App
If everything goes well, you should now be able to start up the Angular CLI development server:
$ ng serve
Make sure you run this from your project’s root directory.
Install Material
Now we’re ready to install Angular Material:
$ npm install @angular/material --save
$ npm install @angular/animations --save
Next, we need to open the client/src/app/app.module.ts file and import the BrowserAnimationsModule
module.
The AppModule
class should now look like this:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserAnimationsModule, BrowserModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Next, let’s import a theme for our app by adding a prebuilt theme to the client/src/styles.scss file:
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
body {
margin: 0;
}
Finally, we are also going to include the Material icons by adding a stylesheet to the client/src/index.html file:
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>