Difference between revisions of "Node.js - Express.js"
Adelo Vieira (talk | contribs) m (Adelo Vieira moved page HTTP REST APIs with Node.js - Express.js to Node.js - Express.js) |
Adelo Vieira (talk | contribs) (→Visual Studio Code) |
||
Line 130: | Line 130: | ||
<br /> | <br /> | ||
− | ==Visual Studio Code== | + | ==[[Visual Studio Code]]== |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
<br /> | <br /> | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
==[[PostgreSQL]]== | ==[[PostgreSQL]]== | ||
Latest revision as of 14:44, 21 December 2020
Contents
- 1 Material del curso
- 2 TypeScript
- 3 Node.js
- 4 Installing TypeScript and ts-node
- 5 Git Bash in Windows
- 6 Visual Studio Code
- 7 PostgreSQL
- 8 Using the Google Chrome developer tools
- 9 TypeScript online compiler
- 10 Creating and Compiling a new TypeScript project
- 11 Express.js
- 12 Database entities
- 13 Connecting to the database from Node.js
- 14 Working with repositories
- 15 Implementing server-side validation with Joi
- 16 The model-view-controller (MVC) design pattern
- 17 Implementing token-based authentication powered by JWT
- 18 Writing automated tests and debugging our Node.js applications
- 19 Resume of command for a new project
- 20 Project - Implementing a REST API with TypeScript and Node.js
Material del curso
Curso online que recomendó Remo: https://javascript30.com/
https://courses.wesbos.com/account/access/5bc5c20a93bbc3208321f21b
Guía 1 - Setting up a Node.js + TypeScript development environment: https://docs.google.com/document/d/15or9Ehxyu0AzTvYHW-LrsVxqZcLY30JjaCGhXAVmmK4/edit#
TypeScript
TypeScript is an open-source programming language developed and maintained by Microsoft. It is a strict syntactical superset of JavaScript, and adds optional static typing to the language.
TypeScript is designed for development of large applications and transcompiles to JavaScript. As TypeScript is a superset of JavaScript, existing JavaScript programs are also valid TypeScript programs. TypeScript may be used to develop JavaScript applications for both client-side and server-side.
To get started with TypeScript installation, you need to first install Node.js in your system.
Node.js
Install Node.js https://nodejs.org/en/download/ https://en.wikipedia.org/wiki/Node.js
Node.js is an open-source, cross-platform JavaScript run-time environment that executes JavaScript code outside of a browser. Historically, JavaScript was used primarily for client-side scripting, in which scripts written in JavaScript are embedded in a webpage's HTML and run client-side by a JavaScript engine in the user's web browser. Node.js lets developers use JavaScript to write Command Line tools and for server-side scripting-running scripts server-side to produce dynamic web page content before the page is sent to the user's web browser. Consequently, Node.js represents a "JavaScript everywhere" paradigm, unifying web application development around a single programming language, rather than different languages for server side and client side scripts.
Installation
Node.js can be installed in different ways:
- Download it from the official site:
- Installing Node.js via package manager:
- https://nodejs.org/en/download/package-manager/
- Debian and Ubuntu based Linux distributions: https://github.com/nodesource/distributions/blob/master/README.md#debinstall
- From Ubuntu repositories:
sudo apt install nodejs
Last time installed the version 12.15 via package manager https://github.com/nodesource/distributions/blob/master/README.md#debinstall
After installation, you can check that Node.js was installed correctly by executing the following command in Bash:
node -v
Node package manager - npm
The Node.js installer will install the node command in your machine but it will also install the Node package manager (npm) command. You can also test that npm has been installed correctly by executing the following command:
npm -v
We can use the npm command to install third-party Node.js packages. The npm command allows us to install a few different kinds of dependencies:
- Global dependencies: Shared across projects. They are installed in our development environment, outside of a project. An example of this kind of dependencies is a CLI tool such as tsc command because we will invoke fro our CLI, not from a project.
- Local Dependencies: This kind of dependencies are installed locally, inside of a project. Not shared across projects. They are needed during the execution of the application; in other words, after the application has been deployed and the development phase has been completed. An example is a framework or library that we use to build our application such as Express.js.
- Development dependencies: Local, not shared across the project and needed only during development. An example is a development tool that we use only during the development phase such as a testing library.
We can install npm dependencies using the following commands:
- Install all the dependencies in the package.json file:
npm install
- Install a global dependency:
npm install -g NAME_OF_THE_DEPENDENCY
- Install a local dependency:
npm install --save NAME_OF_THE_DEPENDENCY
- Install a local development dependency:
npm install --save-dev NAME_OF_THE_DEPENDENCY
You can learn more about npm by reading the official documentation at https://docs.npmjs.com
Installing TypeScript and ts-node
TypeScript and ts-node can be installed using the Node Package Manager (NPM). We will install them as global dependencies:
npm install -g typescript
npm install -g ts-node
Because we have installed both dependencies as global dependencies these will be shared across all projects in our computer. This means that we only need to run these commands once.
You can learn more about npm by reading the official documentation at https://docs.npmjs.com
After installingTypeScript, the command tsc should become available:
- tsc -v
- Transpile your TypeScript code to JavaScript.
- ts-node -v
- ts-node run TypeScript files (*.ts) without first compiling it to plain JavaScript (using tsc) and then use «node» to run the .js
Git Bash in Windows
Si estamos en Windows, tenemos que instalar Git Bash in Windows; que es tan sólo un terminal Bash en Windows.
Visual Studio Code
PostgreSQL
Using the Google Chrome developer tools
We are going to use the Google Chrome developer tools to investigate the potential cause of errors and to call the server-side APIs that we will implement using Express. There are three ways to access the Chrome developer tools:
- Clicking on the top right menu item then going to → More Tools → Developer tools.
- Press F12
- Press Ctrl+ Shift + I
Exploring HTTP requests
We can use the Chrome developer tools to examine all the HTTP request send from the browser. We can do this by accessing the network tab after opening the developer tools. We will then be able to see all the HTTP requests in a list:
If nothing is displayed is most likely because you need to reload the page after opening the developer tools. The network tab also allows us to filter the HTTP request by the content type. For example, if we click in XHR the developer's tools will only display the HTTP requests performed as an AJAX request.
When we click on one of the HTTP requests we are able to explore the details about the requests. Including details such as the request headers, response headers, and status code.
You can learn more about the Chrome developer tools network tab at https://developers.google.com/web/tools/chrome-devtools/network-performance
Executing an HTTP call from the Chrome console
We use ts-node to run our Express applications. However, sometimes we want to call one of the REST web services that we have implemented. For example, we might want to perform the following AJAX call:
(async () => {
const response = await fetch(
"http://www.remojansen.com/website/js/models/awards.json"
);
const json = await response.json();
console.log(json);
})();
We use the fetch function to perform an AJAX call. The best thing about fetch is that it returns a Promise, which means that we can use async/await instead of the Promise.then() method. The only problem is that our browser might not be compatible with async/await because it is one of the latest JavaScript features. So we want to compile this code into JavaScript to avoid potential compatibility issues. We can do this using the tsc command but it is a bit tedious. So instead of that, it is handy to use the TypeScript online compiler (AKA the TypeScript playground): http://www.typescriptlang.org/play/
After we write the TypeScript code we can copy the JavaScript ouput from the right-side editor. We can then open the Chrome developer tools and select the console tab. We can then paste the JavaScript code and press enter to execute it.
You can learn more about the Chrome developer tools console at https://developers.google.com/web/tools/chrome-devtools/console
TypeScript online compiler
TypeScript online compiler (AKA the TypeScript playground): http://www.typescriptlang.org/play/
Creating and Compiling a new TypeScript project
Creating a new TypeScript-Node.js project
Each new TypeScript project requires a new empty folder. It is recommended to create a new folder under your home directory. I call this directory "CODE" and inside it, I create a new empty folder for each project.
A TypeScript project could be a Node.js project but it could also not be a Node.js project. For example, it could be an Angular or React.js project. In this section, we are going to focus on the steps required to create a TypeScript project independently of the kind of application that we will build.
All our TypeScript projects are going to need the following:
- 1- A new empty directory
- 2- A TypeScript compiler configuration file (tsconfig.json)
tsc --init
- 3- A npm configuration file (package.json)
npm init --y
- 4- At this point, we are ready to open the project using VS Code. To open a project in VS Code we need to open VS Code and select File → Open Folder...
- 5- After opening the folder in VS Code, we need to create a new folder. This time we will use the VS Code feature that allows us to create a new folder. I’m going to name the folder src.
- 6- Then I will create a new file named demo.ts using VS Code. The file will be contained inside the src directory.
- 7- Finally, I will add some content to the demo.ts file. In this case, we are going to add just one line of code:
console.log("Hello world!");
Additional steps - Intalling the Node js type definitions
We have learned how to create a basic TypeScript project. However, if we are going to work on a Node.js application, we must perform a few additional steps. First, we need to install the Node.js type definitions. We can do this using npm:
npm install --save-dev @types/node
The Node.js type definition should allow us to import the Node.js core modules. For example, we can import the file system module as follow:
import * as fs from "fs";
We are also going to open the tsconfig.json and uncomment the "lib" setting and add the following value:
"lib": [ "es6" ]
The preceding should allow us to work with some of the JavaScript APIS of the ES6 specification such as the Promise API.
You will need to repeat these steps everytime you create a new TypeScript project. It is very important to use a new empty folder to avoid potential configuration issues.
Transpiling and running a TypeScript project
Now that we have created the basic structure of a TypeScript project we are going to compile it and execute it. To compile the TypeScript project we need to use the tsc command and the project flag to specify the TypeScript compiler configuration file:
tsc -p tsconfig.json
If everything goes well this will generate a new file named demo.js right next to the existing demo.ts file. We can then execute the JavaScript file demo.js using the node command:
node ./src/demo.js
Please note that the node command can only be used to execute JavaScript files (.js) not TypeScript files (.ts) this means that we must compile our application before we can run it. This requires two steps and it is a bit tedious. However, we can use the ts-node command to do the two steps with one single command:
ts-node ./src/demo.ts
Note that the ts-node command can be used with TypeScript files.
Express.js
Express.js, or simply Express, is a web application framework for Node.js, released as free and open-source software under the MIT License. It is designed for building web applications and APIs. It has been called the de facto standard server framework for Node.js.
What is a Library and a framework
The key difference between a library and a framework is "Inversion of Control". When you call a method from a library, you are in control. But with a framework, the control is inverted: the framework calls you.
A library is just a collection of class definitions. The reason behind is simply code reuse, i.e. get the code that has already been written by other developers. The classes and methods normally define specific operations in a domain specific area. For example, there are some libraries of mathematics which can let developer just call the function without redo the implementation of how an algorithm works.
In framework, all the control flow is already there, and there's a bunch of predefined white spots that you should fill out with your code. A framework is normally more complex. It defines a skeleton where the application defines its own features to fill out the skeleton. In this way, your code will be called by the framework when appropriately. The benefit is that developers do not need to worry about if a design is good or not, but just about implementing domain specific functions.
HTTP web services with Express.js
We are going to use Express.js to declare REST Web Services. A web service is a piece of code that can be invoked remotely by a consumer (known as the client). We are going to build web services using the HTML protocol. We will use a unique URL for each web service and we will use HTTP requests and responses (with headers and body) to transfer data between the client and the server.
It is very important to understand that we are going to use Express.js as a framework that helps us to implement web services. This means that Express is used to implement the server part (not the client).
When we are working with Express, we will declare web services that always receive HTTP requests and answer with HTTP responses. Express will never receive a response because that only happens in the client.
Express will run as a process in our machine. We will execute this process using the terminal. However, the client will be just some code that we will execute in our web browser.
Please refer to the An Introduction to API's by Gonzalo Vázquez article if you need additional help to understand the HTTP protocol and Web APIs as a concept.
Installing the express module and its type definitions
We now know how to create a generic TypeScript Node.js project, but we need to be a little more specific. We are going to focus on bulding Node.js applications using a framework know as Express.js.
To be able to use Express, we need to install the express module and its type definitions @types/express:
npm install --save express
npm install --save-dev @types/express
We can then import express as follows:
import express from "express";
This will fail if you forget to install both modules.
Express Hello World
At this point, we should be able to create a very basic Express application and execute it to see if everything has gone right so far. We are going to change the content of the demo.ts file (file that we created when we tested the new TypeScript-node project) from:
console.log("Hello world!");
To:
// Import express module
import express from "express";
// Declare a new express application
const app = express()
// Declare port to be used by the server
const port = 8080
// Declare endpoint for HTTP GET /
app.get('/', (req, res) => res.send('Hello World!'));
// Start HTTP server in port 8080
app.listen(
port,
() => console.log(`Example app listening on port ${port}!`)
);
We can then run the Express application using:
ts-node ./src/demo.ts
If everything goes well the following should be displayed in bash:
Example app listening on port 3000!
You can then open Google Chrome and visit http://localhost:3030 and if everything went well you should see the following on the screen:
Hello World!
The previous code snippet represents the minimum amount of code required to start an Express application. The code snippet declares a new express application and our very first web service. The address path “/” will return “Hello World!” for HTTP reuquests with GET as its method. This is our very first web service. The Express application then starts running in the port 8080.
The preceding code snipped creates an HTTP server and starts running it. Once the server is running you cannot use the bash anymore unless you kill the process. You can kill the process by pressing Ctrl + c when focus on the bash.
Please note that if you change the code you will need to:
- Save the changes
- Kill the process using Ctrl + c
- Rerun it with ts-node
Later in this document, we will learn a way to automatically re-load the server when the code changes but it is important to know how to do it by hand.
Automatically restart the server when the code changes
Install an npm module known as nodemon:
sudo npm install -g nodemon
Create a nodemon.json file with the following content:
nodemon.json
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node ./src/demo.ts"
}
Run the application using nodemon:
nodemon
The nodemon process will use the configuration from the nodemon.json file. This configuration will watch changes in the src folder and execute the ts-node ./src/demo.ts command every time there is a change. By doing this you will not need to worry about manually killing the server and starting it again everytime you change a file.
Declaring endpoints
Now we are going to learn how to declare HTTP endpoints for a REST Web API that allow us to manage movies. Instead of using a real database we are going to use an array of movies stored in memory.
HTTP GET - The server
The following code snippet demonstrates how to declare HTTP GET endpoints:
import express from "express";
import * as bodyParser from "body-parser";
// Creates app
const app = express();
// Declare default HTTP GET endpoint
app.get("/", (req, res) => {
res.send("This is the home page!");
});
// Declare a list of movies using an in-memory array
let movies = [
{
title: "Titanic",
year: 1997
},
{
title: "A star is born",
year: 2018
}
];
// Declare HTTP GET endpoint to get movies
app.get("/movies", (req, res) => {
res.json(movies);
});
// INSERT MORE CODE HERE...
// Start the application
app.listen(8080, () => {
console.log(
"The server is running in port 8080!"
);
});
Sending arguments to the server - The server
The following code snippet demonstrates how to declare an HTTP GET endpoint that expects a request parameter named "movieId":
// ...
// Declare HTTP GET endpoint to get a movie by movie ID
app.get("/movies/:movieId", (req, res) => {
const movieId = req.params.movieId;
res.json(movies[movieId]);
});
// ...
http://localhost:8080/movies/1
Calling the endpoint - The client
We can call the endpoint by opening the http://localhost:8080 address in our browser, opening the developer tools console and executing the compilation output of the following code:
(async () => {
const response = await fetch(
"http://localhost:8080/movies",
{
method: "GET"
}
);
const json = await response.json();
console.log(json);
})();
Remember that you need to compile this code before you can execute it in the console. You can refer to the the "Setting up a Node.js + TypeScript development environment" guide if you need additional help with that task:
- To compile (convert from ts to js): Go to http://www.typescriptlang.org/play/
- Then we need to go to http://localhost:8080, open the tools console (Ctrl + Shift + i) and executing the js code generated. To do so just paste the code in the console and press enter.
HTTP POST - The server
The following code snippet demonstrates how to declare an HTTP POST endpoint that expects a movie in JSON format in the request body.
Please note that you will need to install the following packages:
npm install --save body-parser
npm install --save @types/body-parser
In order to be able to send JSON in the request body, we need to first enable some configuration using a package known as body parser.
We can then set the configuration and declare the HTTP POST endpoint:
import express from "express";
import bodyParser from "body-parser";
// Creates app
const app = express();
// Enable JSON body in requests
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Declare HTTP POST endpoint to create a movie
app.post("/movies", (req, res) => {
const newMovie = req.body;
movies.push(newMovie);
res.json(newMovie);
});
// ...
Sending JSON to the server - The client
Just like before, we can call the endpoint by opening the http://localhost:8080 address in out browser, opening the developer tools console and executing the compilation output of the following code:
(async () => {
const data = {
title: "Alien",
year: 1979
};
const response = await fetch(
"http://localhost:8080/movies",
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(data)
}
);
const json = await response.json();
console.log(json);
})();
After compilation you can check that the new movie has been added to the array of movies refreshing http://localhost:8080/movies
HTTP PUT - The server
The following code snippet demonstrates how to declare HTTP PUT endpoints:
// ...
app.put("/movies", (req, res) => {
const newMovie = req.body;
movies.push(newMovie);
res.json(newMovie);
});
// ...
HTTP DELETE (The server)
The following code snippet demonstrates how to declare HTTP DELETE endpoints:
// ...
app.delete("/movies/:movieId", (req, res) => {
const movieId = req.params.movieId as string;
movies = movies.filter((movie, index) => {
// console.log(index, movieId, index.toString() !== movieId);
return index.toString() !== movieId;
});
res.json(movies);
});
// ...
Please note that when we delete a movie we need to provide the movie ID as a request parameter.
Database entities
Installing TypeORM
TypeORM is an object-relational mapping library. It allow us to map the data from the format used by the database (rows of data) to the format used in our application code (class definitions). This allow us to not have to think about the database details such as the SQL syntax while we implement our applications.
In order to be able to use TypeORM we are going to need to install the following dependencies (in addition to the dependencies that are pre-required for an Express application as we explored in some of the previous tutorials):
npm install typeorm
npm install pg
npm install reflect-metadata
Note: You need to do this inside the demo folder that we created earlier
We also need to ensure that we update the following settings in the TypeScript compiler configuration file (tsconfig.json):
"lib": ["es6", "dom"],
// ...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
Creating database entities
The following code snippet should be added to a new file named movie.ts. We need to import some decorators from the TypeOrm module. In TypeScript a decorator is a function that can be applied using the @ symbol. Decorators can be applied to classes and its methods and properties:
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Movie {
@PrimaryGeneratedColumn()
public id!: number;
@Column()
public title!: string;
@Column()
public year!: number;
}
The preceding code snippet uses the following decorators:
- @Entity() This decorator must be applied to a class and it is used to generate a table in our database. The Movie class will become a table named movie in our database.
- @PrimaryGeneratedColumn() This decorator must be applied to a property and it is used to generate a primary key column in the database. The primary key will be numeric and will increment using auto-generated incremental numeric values.
- @Column() This decorators must be applied to a property and it is used to generate a column in the database. The data type of the column will be automatically selected based on the data type of the properties of the class.
Connecting to the database from Node.js
The following code snippet declares a new Express application just like we did in the preceding tutorials but this time we are going to have some additional code that uses TypeORM to establish a connection to the database and create a repository for the Movie entity.
Note: the following code should be the content of the file /src/demo.ts
import express from "express";
import bodyParser from "body-parser";
import { createConnection, getConnection } from "typeorm";
import { Movie } from "./movie";
(async () => {
// Read environment variables
const DATABASE_HOST = process.env.DATABASE_HOST;
const DATABASE_PASSWORD = process.env.DATABASE_PASSWORD;
const DATABASE_USER = process.env.DATABASE_USER;
const DATABASE_DB = process.env.DATABASE_DB;
// Display connection ditails in console
console.log(
`
host: ${DATABASE_HOST}
password: ${DATABASE_PASSWORD}
user: ${DATABASE_USER}
db: ${DATABASE_DB}
`
);
// Open a database connection
await createConnection({
type: "postgres",
host: DATABASE_HOST,
port: 5432,
username: DATABASE_USER,
password: DATABASE_PASSWORD,
database: DATABASE_DB,
entities: [
Movie
],
synchronize: true
});
// Get the opened connection and create a repository
const movieRepository = getConnection().getRepository(Movie);
// Creates app
const app = express();
// Use the body parse middlerare so we can send JSON
// In the HTTP requests body
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Declare the home page endpoint
app.get("/", (req, res) => {
res.send("This is the home page!");
});
// TODO We will declare HTTP endpoints here!
// Run the express HTTP server in port 8080
app.listen(8080, () => {
console.log(
"The server is running in port 8080!"
);
});
})();
Note how the createConnection takes the previously defined entities as one of its arguments as well as a setting named synchronize. The synchronize setting will create a database table for the Movie entity if it doesn’t already exist in our database.
Working with repositories
As we have learned in the preceding code snippet, we can create a repository by invoking the getRepository method in a connection. We must pass an entity and we can get the connection using the getConnection function:
const movieRepository = getConnection().getRepository(Movie);
We must create one repository for each Entity. Later we will see that repositories are usually created in a different file (movie_repository.ts, for example) with the following contents (See Interactive Web Applications#The model-view-controller (MVC) design pattern):
movie_repository.ts
import { getConnection } from "typeorm";
import { Movie } from "../entities/movie";
export function getMovieRepository() {
const connection = getConnection();
const movieRepository = connection.getRepository(Movie);
return movieRepository;
}
The preceding file declares a function that creates a repository for the Movie entity using an existing connection. The repository allows us to perform database operations such as reading rows or deleting rows.
We are now going to declare some HTTP web services that allow us to manage movies stored in our database. We will extend the preceding code snippet by adding code into the section that was used as a placeholder:
// TODO We will declare HTTP endpoints here!
Reading multiple entities from the database
The following code snippet declares an HTTP endpoint that returns all the movies in the database. We use the method "find" in the movieRepository to read all the rows in the movies table:
app.get("/movies", (req, res) => {
(async () => {
const movies = await movieRepository.find();
res.json(movies);
})();
});
We can check that this is working by accessing the following URL: http://localhost:8080/movies
Please remember that you must save the files and restart the application for this changes to be effective. At first, the server should return an empty array because there are no rows in the database yet. We can use SQLectron to insert some data into the database by hand. To do so, we need to open SQLectron and select the connection that we created previously and execute some SQL commands by hand. For example, the following SQL statement can be used to insert a row into the movie table:
INSERT INTO "public"."movie" ("title", "year")
VALUES ('Alien', 1979);
The movie table is auto-generated by TypeORM when we create a connection.
We can explore our database from SQLectron. On the left side, we can see all the databases in our database server. We can click on the demo database to expand it. The TypeORM tables are generated under a schema named public. We can click on the schema to see the movie table and click on the table to see its columns. If you can’t remember how to write SQL commands by hand you can right-click on one of the tables and select an operation such as insert. A template insert statement for the selected table will be then pasted into the editor.
Reading one entity by its ID from the database
We can read one entity from the database by its ID by passing the ID as a request parameter. We can then use the findOne method in the repository class:
app.get("/movies/:movieID", (req, res) => {
(async () => {
const movieIdStr = req.params.movieID as string;
const movieIdNbr = parseInt(movieIdStr);
if (isNaN(movieIdNbr)) {
res.status(400).send({
msg: "Id must be a number!"
});
}
const movies = await movieRepository.findOne(movieIdNbr);
res.json(movies);
})();
});
We can check that this is working by accessing the following URL:
http://localhost:8080/movies/1
Once more, we must insert a movie with the ID 1 into our database before this can work.
Deleting an entity from the database by its ID
We can delete an entity using the HTTP DELETE method and the delete method in the repository class:
app.delete("/movies/:movieId", (req, res) => {
const movieIdStr = req.params.movieId as string;
movieRepository.delete(movieIdStr);
res.json(true);
});
We can invoke the preceding service using the following client-side code:
(async () => {
const response = await fetch(
"http://localhost:8080/movies/1",
{
method: "DELETE"
}
);
const json = await response.json();
console.log(json);
})();
Creating a new entity
We use the HTTP POST method to create new entities. The following code snippet demonstrates how we can use the save method of the repository to save a new entity. The following code expects a valid movie instance to be sent to the server via the HTTP request body:
app.post("/movies", (req, res) => {
(async () => {
const newMovie = req.body;
const movies = await movieRepository.save(newMovie);
res.json(movies);
})();
})
It is important to remember to add the HTTP headers and to send the new entity as a JSON string when we invoke the preceding web service from a web client:
(async () => {
const newMovie = {
title: "2001: A Space Odyssey",
year: 1968
};
const response = await fetch(
"http://localhost:8080/movies/1",
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=UTF-8"
},
body: JSON.stringify(newMovie)
}
);
const json = await response.json();
console.log(json);
})();
Updating an existing entity
There are two main ways to update an entity. We can update an entity partially or fully.
Full update
Full update (HTTP PUT)
The following code snippet is very similar to the HTTP POST example. This time we use the HTTP PUT method. We will send a movie to the server as JSON using the HTTP request body. We then read a movie from the database using the ID of the movie in the request body and we update each one of its fields. Finally, we save it. We use the repository methods findOne and save:
app.put("/movies/:movieId", (req, res) => {
(async () => {
const movieId = req.params.movieId;
const update = req.body;
const oldMovie = await movieRepository.findOne(movieId);
if (oldMovie) {
oldMovie.title = update.title;
oldMovie.year = update.year;
const updatedMovie = await movieRepository.save(oldMovie);
res.json(updatedMovie);
} else {
res.status(404).send();
}
})();
});
If no movies can be found with the provided ID we return an HTTP error 404. If no errors take place we return the updated movie.
Partial update (HTTP PATCH)
The HTTP PATH method is almost identical to the HTTP PUT method. The main difference is that the request body will not contain a full movie. It will only contain the fields that we wish to update. So instead of containing something like the following:
{
id: 1,
title: "Alien",
year: 1979
}
It could contain something like the following:
{ year: 1979 }
The implementation uses the findOne and save methods from the repository class:
app.patch("/movies/:movieId", (req, res) => {
(async () => {
const movieId = req.params.movieId;
const update = req.body;
const oldMovie = await movieRepository.findOne(movieId);
if (oldMovie) {
const key = Object.keys(update)[0];
const val = update[key];
(oldMovie as any)[key] = val;
const updatedMovie = await movieRepository.save(oldMovie);
res.json(updatedMovie);
} else {
res.status(404).send();
}
})();
});
We need to use a casting to be able to set the one of the properties of Movie using index key property access ([key]).
Please refer to the following links if you need additional help to understand how the preceding code snippet works:
Please note that we have avoided implementing validation in all the preceding examples but we should implement it to prevent potential security vulnerabilities and to provide the clients of the API with friendly feedback. For example of a client sends a HTTP PATCH request with { description: "Something..." } in the body, we should throw a 400 error that explains that "description" is not a property of movie.
Implementing server-side validation with Joi
https://www.npmjs.com/package/joi
https://www.npmjs.com/package/@hapi/joi
https://www.npmjs.com/package/react-joi-validation
Before we can get started with Joi we need to install it together with its type definitions:
npm install joi
npm install @types/joi
We then need to import is as follows:
import * as joi from "joi";
We can use joi to validate that an object adheres to certain rules. The validation rules are defined in an object known as a schema. The following example declares a schema that validates a user with an email and password. After invoking the validate function we need to check whether the result contains an error or not:
import * as joi from "joi";
const user = {
email: "test@demo.com",
password: "secret!"
};
const userSchema = {
email: joi.string().email(),
password: joi.string()
};
const result = joi.validate(user, userSchema);
if (result.error) {
// User is invalid
} else {
// Do something
}
The model-view-controller (MVC) design pattern
The model-view-controller (MVC) design pattern is defined as follows by MSDN:
The Model-View-Controller (MVC) architectural pattern separates an application into three main components: the model, the view, and the controller. MVC is a standard design pattern that many developers are familiar with. Some types of Web applications will benefit from the MVC framework.
The MVC pattern includes the following components:
- Models: Model objects are the parts of the application that implement the logic for the application's data domain. Often, model objects retrieve and store model state in a database.
- Views: Views are the components that display the application's user interface (UI). Typically, this UI is created from the model data.
- Controllers: Controllers are the components that handle user interaction, work with the model, and ultimately select a view to render that displays UI. In an MVC application, the view only displays information; the controller handles and responds to user input and interaction. For example, the controller handles query-string values and passes these values to the model, which in turn might use these values to query the database.
The MVC pattern helps you create applications that separate the different aspects of the application (input logic, business logic, and UI logic) while providing a loose coupling between these elements. The pattern specifies where each kind of logic should be located in the application. The UI logic belongs in the view. Input logic belongs in the controller. Business logic belongs in the model. This separation helps you manage complexity when you build an application because it enables you to focus on one aspect of the implementation at a time.
The loose coupling between the three main components of an MVC application also promotes parallel development. For example, one developer can work on the view, a second developer can work on the controller logic, and a third developer can focus on the business logic in the model.
Implementing MVC with Express
The example that we are about to implement contains the following files:
├── src
│ ├── controllers
│ │ └── movie_controller.ts
│ ├── entities
│ │ └── movie.ts
│ ├── repositories
│ │ └── movie_repository.ts
│ ├── config
│ │ └── db.ts
│ └── main.ts
├── package.json
└── tsconfig.json
Config
The first thing that we are going to do is to move the database connection from the main file in our application to a file named db.ts under the src/config directory. The file should contain the following content:
import { createConnection } from "typeorm";
import { Movie } from "../entities/movie";
import { Director } from "../entities/director";
import { User } from "../entities/user";
export async function createDbConnection() {
// Read the database settings from the environment variables
const DATABASE_HOST = process.env.DATABASE_HOST;
const DATABASE_PASSWORD = process.env.DATABASE_PASSWORD;
const DATABASE_USER = process.env.DATABASE_USER;
const DATABASE_DB = process.env.DATABASE_DB;
// Display the settings in the console so we can see if something is wrong
console.log(
`
host: ${DATABASE_HOST}
password: ${DATABASE_PASSWORD}
user: ${DATABASE_USER}
db: ${DATABASE_DB}
`
);
// Open database connection
await createConnection({
type: "postgres",
host: DATABASE_HOST,
port: 5432,
username: DATABASE_USER,
password: DATABASE_PASSWORD,
database: DATABASE_DB,
// If you forget to add your entities here you will get a "repository not found" error
entities: [
Movie,
Director,
User
],
// This setting will automatically create database tables in the database server
synchronize: true
});
}
Model
Our "model" is going to be composed of two layers: entities and repositories. We are going to implement an entity and a repository that allows us to work with movies.
Entities
The entity is implemented using TypeORM.
Once you have your environment ready we need to declare the Movie entity. We need to create a file named movie.ts under the src/entities directory with the following contents:
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Movie {
@PrimaryGeneratedColumn()
public id!: number;
@Column()
public title!: string;
@Column()
public year!: number;
}
This file is used by TypeORM to automatically generate tables in our database.
Repositories
After creating our entities we must create one repository for each one of them. We need to create a file named movie_repository.ts under the src/repositories directory with the following contents:
import { getConnection } from "typeorm";
import { Movie } from "../entities/movie";
export function getMovieRepository() {
const connection = getConnection();
const movieRepository = connection.getRepository(Movie);
return movieRepository;
}
The preceding file declares a function that creates a repository for the Movie entity using an existing connection. The repository allows us to perform database operations such as reading rows or deleting rows.
Please note that the repository code will fail if we forget to add the entity to the entities option when we create the database connection in the src/config/db.ts file:
// ...
await createConnection({
type: "postgres",
host: DATABASE_HOST,
port: 5432,
username: DATABASE_USER,
password: DATABASE_PASSWORD,
database: DATABASE_DB,
entities: [
Movie // Add the entity we create the database connection
],
synchronize: true
});
// ...
Controller
After creating our repository we are ready to create a controller. A controller is one of the layers in the MVC architecture and it is in charge of processing the user input. Because we are building HTTP REST APIs the user input will always take the form of an HTTP request. The controller will be in charge of processing the HTTP requests that arrive at the service and it will then delegate part of the work to the model layer.
The following code snippet declares a controller that declares multiple HTTP endpoints that can be used to perform multiple operations such as creating a new movie or reading all movies. This file should be named movie_controller.ts and it should be contained under the src/controllers directory.
import * as express from "express";
import * as joi from "joi";
import { authMiddleware, loggerMiddleware } from "../config/middleware";
import { getMovieRepository } from "../repositories/movie_repository";
export function getMovieController() {
// Create a repository so we can perform database operations
const movieRepository = getMovieRepository();
// Create router instance so we can declare enpoints
const router = express.Router();
// Declare Joi Schema so we can validate movies
const movieSchemaForPost = {
title: joi.string(),
year: joi.number().greater(0)
};
// HTTP GET http://localhost:8080/movies/
router.get("/", (req, res) => {
(async () => {
const movies = await movieRepository.find();
res.json(movies);
})();
});
// HTTP GET http://localhost:8080/movies/1
router.get("/:id", (req, res) => {
(async () => {
const id = req.params.id;
const movies = await movieRepository.findOne(id);
res.json(movies);
})();
});
// HTTP GET http://localhost:8080/movies/by_year/1979
router.get("/by_year/:year", (req, res) => {
(async () => {
const year = req.params.year;
const movies = await movieRepository.find({ year: year });
res.json(movies);
})();
});
// HTTP DELETE http://localhost:8080/movies/1
router.delete("/:id", authMiddleware, (req, res) => {
(async () => {
const userId = (req as any).userId;
const id = req.params.id;
const movies = await movieRepository.delete(id);
res.json(movies);
})();
});
// HTTP POST http://localhost:8080/movies/
router.post("/", authMiddleware, (req, res) => {
(async () => {
const newMovie = req.body;
const result = joi.validate(req.body, movieSchemaForPost);
if (result.error) {
res.status(400).send({ msg: "Movie is not valid!" });
} else {
const movies = await movieRepository.save(newMovie);
res.json(movies);
}
})();
});
return router;
}
Routing and controllers
There is one very important thing that we need to understand. When we create a new router, it will be used to handle a subset of HTTP requests. The subset will be directly related with one of the controllers. For example, all the HTTP requests that start with http://localhost:8080/movies/ will be handled by the movies controller. However, if you examine the movies controller you will see that the path to get one movie by ID is declared as "/:id" not as "/movies/:id" however, we are going to invoke it using the http://localhost:8080/movies/1 URL. So you might be wondering where is the "/movies" part of the URL declared? The answer is the application's entry point. In the following section, we are going to take a look at the application's entry point and we will see the following code snippet:
const moviesController = getMovieController();
app.use("/movies", moviesController);
As we can see we are linking the path "/movies" to the Movies Controller. The movies controller then declares the path endpoint to get one Movie by ID: "/:id". The Express routing system will put together these two pieces of configuration and determine that to get a movie by ID we need to use both "/movies" + "/:id". This explains why we don’t need to declare "/movies" in our controller.
The application's entry point
After moving our database config to a separated file, declaring an entity, a repository and a controller we are finally ready to update the applications entry point. This file is the root file in our application. It is the file that puts together all the other files in our application.
The following file performs the following operations:
- Create a database connection
- Create an express application
- Configure the express application to work with JSON
- Declare the main HTTP endpoint
- Declare HTTP endpoints for each of the controllers in the application
- Start the express server in port 8081
The entire source code of the applications entry point can be found in the following code snippet. This file should be named main.ts and it should be contained under the src directory.
import express from "express";
import bodyParser from "body-parser";
import { getMovieController } from "./controllers/movie_controller";
import { createDbConnection } from "./config/db";
(async () => {
// Create db connection
await createDbConnection();
// Creates app
const app = express();
// Server config to be able to send JSON
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Declare the main path
app.get("/", (req, res) => {
res.send("This is the home page!");
});
// Declare controllers
const moviesController = getMovieController();
app.use("/movies", moviesController);
// Start the server
app.listen(8080, () => {
console.log(
"The server is running in port 8080!"
);
});
})();
At this point, you should be able to run the application. You can run the application using one of the methods that we described in the preceding episodes. Remember that you have three options:
- Compile first with tsc and run after with node
- Compile and run in one step with ts-node
- Automatically compile and re-load the application with ts-node and nodemon
We now have an HTTP API that works with a real database and implements the MVC design pattern. The next step is to add user authentication to this application and we are going to do this using an Express feature known as middleware.
Implementing token-based authentication powered by JWT
We are going to take continue working on the code that we have created during the preceding sections. We are going to need to add some new files (displayed in 'red' below) and to edit some existing files (displayed in "red" below):
├── src
│ ├── controllers
│ │ ├── 'auth_controller.ts'
│ │ ├── 'user_controller.ts'
│ │ └── movie_controller.ts
│ ├── entities
│ │ ├── 'user.ts'
│ │ └── movie.ts
│ ├── repositories
│ │ ├── 'user_repository.ts'
│ │ └── movie_repository.ts
│ ├── config
│ │ ├── 'middleware.ts'
│ │ └── "db.ts"
│ └── "main.ts"
├── package.json
└── tsconfig.json
The token-based authentication is implemented using the following workflow:
- The user sends its credentials (email and password) to the server using an HTTP POST request.
- The server searches in the database for a user with the given email and password. If a user is found, a JSON Web Token (JWT) is created. Creating a token is performed using a secret key that only the server knows. The token contains the user ID.
- The token is shared with the user in the response body of the original HTTP POST request.
- When the user wants to perform an action that requires authentication, it will provide the JWT as a request header. For example, an HTTP DELETE request to delete a movie will contain an additional header named “x-auth-token” that contains the JWT.
- If the header is present the server will try to verify it. The token verification also requires the secret key that only the server knows. If the token is valid we don’t need to read the database to identify the current user because its ID is contained in the Token.
Prerequisite - Creating users accounts
Before we can implement login we need to allow our users to create an account. So we are going to create a database table for users and we are going to create a controller for Users that will allow us to create user accounts. We are not going to detail too much these steps because they are almost identical to the steps that we followed when we declared the movie controller. We will start by declaring an entity named User.
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
public id!: number;
@Column()
public email!: string;
@Column()
public password!: string;
}
We then need to create a repository for the User entity.
import { getConnection } from "typeorm";
import { User } from "../entities/user";
export function getUserRepository() {
const connection = getConnection();
const userRepository = connection.getRepository(User);
return userRepository;
}
It is important to not forget that we need to add the new entity to our database connection in the /src/config/db.ts file:
// ...
await createConnection({
type: "postgres",
host: DATABASE_HOST,
port: 5432,
username: DATABASE_USER,
password: DATABASE_PASSWORD,
database: DATABASE_DB,
entities: [
Movie,
User // Add the new entity to our database connection
],
synchronize: true
});
// ...
At this point, we are ready to implement the user controller. This controller only implements an endpoint that allows us to create a new user account. The following content should be added to the user_controller.ts file under the controllers directory:
import * as express from "express";
import { getUserRepository } from "../repositories/user_repository";
import * as joi from "joi";
export function getUserController() {
const userRepository = getUserRepository();
const router = express.Router();
const userDetailsSchema = {
email: joi.string().email(),
password: joi.string()
};
// HTTP POST http://localhost:8080/users/
router.post("/", (req, res) => {
(async () => {
const newUser = req.body;
const result = joi.validate(newUser, userDetailsSchema);
if (result.error) {
res.status(400).send();
} else {
const user = await userRepository.save(newUser);
res.json({ ok: "ok" }).send();
}
})();
});
return router;
}
Please don't forget that we need to add the controllers to the application's entry point:
// ...
const moviesController = getMovieController();
app.use("/movies", moviesController);
const usersController = getUsersController();
app.use("/users", usersController);
// ...
We can invoke the endpoint from a web browser using the following code. Please remember that you need to compile the code into JavaScript before you can run it:
(async () => {
const data = {
email: "test@demo.com",
password: "secret1"
};
const response = await fetch(
"http://localhost:8080/users/",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}
);
const json = await response.json();
console.log(json);
})();
Auth middleware
In Express, a Middleware is an especial function that takes three arguments:
- An HTTP request
- An HTTP response
- A function that is known as "next"
The following code snippet implements a middleware function that displays "Hello from middleware!" in the console:
import * as express from "express";
function helloMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
console.log("Hello from middleware!");
next();
}
The helloMiddleware function is a middleware function. This middleware function logs some text in the console and invokes the next function. To understand what is the "next" function, we need to learn how we can apply a middleware. A middleware function can be applied to an endpoint by passing it as the second argument when we declare an HTTP endpoint:
//...
router.post("/", helloMiddleware, (req, res) => {
// ...
});
// ...
For example, the preceding code snippet declares an HTTP POST endpoint for the path "/". We can pass the middleware function as the second argument. The middleware will be invoked first when an HTTP POST request hits the "/" path. After, the middleware function is executed the next function might be invoked by the middleware function. The next function triggers the endpoint requests handler to be executed.
This means that if the middleware doesn't invoke the next function the endpoint handler is never executed. This is the case of the auth middleware function. We use the auth middleware function to check if the HTTP requests contain a valid JWT in one of the request headers. If the token is not present or invalid we never invoke the next function which means that the endpoint handler is never executed.
The following code snippet implements the auth middleware. It demonstrates how we can read the request header, verify a token using a secret key stored in an environment variable and invoke the next action. The following content should be added to the middleware.ts file under the config directory:
import * as express from "express";
import jwt from "jsonwebtoken";
// Middleware function used for JWT token validation
export function authMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
// Read token signature from environment variables
const AUTH_SECRET = process.env.AUTH_SECRET;
// Read token from request headers
const token = req.headers["x-auth-token"];
// Client error if no token found in request headers
if (typeof token !== "string") {
res.status(400).send();
} else {
// Server error is enironment variable is not set
if (AUTH_SECRET === undefined) {
res.status(500).send();
} else {
try {
// Check that the token is valid
const obj = jwt.verify(token, AUTH_SECRET) as any;
// Add the user ID to the HTTP request object so we can access it from the NEXT request handler
(req as any).userId = obj.id;
// Invoke NEXT request handler
next();
} catch(err) {
// Unauthorized if the token cannot be verified
res.status(401).send();
}
}
}
}
Private endpoints
We can apply the auth middleware function to the endpoints that we want to make private. We say that these endpoints are private because they cannot be invoked without a valid JWT.
// ...
// HTTP DELETE http://localhost:8080/movies/1
router.delete("/:id", authMiddleware, (req, res) => {
(async () => {
const userId = (req as any).userId;
const id = req.params.id;
const movies = await movieRepository.delete(id);
res.json(movies);
})();
});
// HTTP POST http://localhost:8080/movies/
router.post("/", authMiddleware, (req, res) => {
(async () => {
const newMovie = req.body;
const result = joi.validate(req.body, movieSchemaForPost);
if (result.error) {
res.status(400).send({ msg: "Movie is not valid!" });
} else {
const movies = await movieRepository.save(newMovie);
res.json(movies);
}
})();
});
// ...
To invoke a private endpoint from the web browser we will need to provide a valid JWT as one of the request headers as demonstrated by the following code snippet. Please remember that you need to compile the following code snippet before you can execute it in a web browser:
(async () => {
const response = await fetch(
"http://localhost:8080/movies/1",
{
method: "DELETE",
headers: {
"x-auth-token": "INSERT_YOUR_JWT_HERE"
}
}
);
const json = await response.json();
console.log(json);
})();
However, as you can imagine, you need to get a valid JWT before you can perform the preceding operation. To get a JWT we need to pass a valid user email and password to the auth controller. In the next section, we are going to learn how to implement it.
Login
The login endpoint is declared by the auth controller. The login endpoint tries to search for an existing user in our database and creates a JWT that contains the user ID. The following content should be added to the auth_controller.ts file under the controllers directory:
import * as express from "express";
import { getUserRepository } from "../repositories/user_repository";
import * as joi from "joi";
import jwt from "jsonwebtoken";
export function getAuthController() {
const AUTH_SECRET = process.env.AUTH_SECRET;
const userRepository = getUserRepository();
const router = express.Router();
const userDetailsSchema = {
email: joi.string().email(),
password: joi.string()
};
// HTTP POST http://localhost:8080/auth/login/
router.post("/login", (req, res) => {
(async () => {
const userDetails = req.body;
const result = joi.validate(userDetails, userDetailsSchema);
if (result.error) {
res.status(400).send();
} else {
const match = await userRepository.findOne(userDetails);
if (match === undefined) {
res.status(401).send();
} else {
if (AUTH_SECRET === undefined) {
res.status(500).send();
} else {
const token = jwt.sign({ id: match.id }, AUTH_SECRET);
res.json({ token: token }).send();
}
}
}
})();
});
return router;
}
Please don't forget that we need to add the controllers to the application's entry point:
// …
const moviesController = getMovieController();
app.use("/movies", moviesController);
const usersController = getUsersController();
app.use("/users", usersController);
const authController = getAuthController();
app.use("/auth", authController);
// ...
We can invoke the preceding endpoint from the web browser using the following fetch call. Please remember that you need to compile the following code snippet before you can execute it in a web browser:
(async () => {
const data = {
email: "test@demo.com",
password: "secret1"
};
const response = await fetch(
"http://localhost:8080/auth/login//",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}
);
const json = await response.json();
console.log(json);
})();
Writing automated tests and debugging our Node.js applications
https://codeburst.io/javascript-unit-testing-using-mocha-and-chai-1d97d9f18e71
https://tutorialedge.net/typescript/testing-typescript-api-with-mocha-chai/
Writing unit tests
We are going to write some automated test using mocha and chai as our main testing framework. We can install these librariesusing the npminstall command:
sudo npm install mocha chai @types/mocha @types/chai nyc typescript ts-node
Please note that the preceding code snippet will also install the nyc command as well as TypeScript and ts-node locally. We installed previously TypeScript and ts-node as global dependencies with npm install -g however, this time we are going to install them as local dependencies because it is required by the nyc command.
The next step is to edit the scripts section in our «package.json» file:
"scripts": {
"test": "INSERT TEST COMMAND HERE"
},
We need to replace the text «INSERT TEST COMMAND HERE» with our test command:
nyc --clean --all
--require ts-node/register
--require reflect-metadata/Reflect
--extension .ts
--mocha --timeout 5000 **/*.test.ts
Please note that line breaks should be removed. They have been added here for formatting purposes only.
The preceding command will execute all mocha tests contained in files with the extension .test.ts. This command also collects test coverage data that we can later use to generate a test coverage report. A test coverage report is a useful source of information because it allows us to identify parts of our application that have not been tested.
After installing some dependencies, we are going to write our first couple unit tests.
The preceding code snippet is testing the Calculator class which is described in the /src/calculator.ts file:
./src/calculator.ts
export class Calculator {
public add(a: number, b:number) {
throw new Error();
}
public div(a: number, b: number) {
throw new Error();
}
}
We are going to create a folder named tests on the root of our project and create a file named calculator.test.ts inside it. The calculator.test.ts file should contain the following source code:
./tests/calculator.test.ts
import { expect } from "chai";
import {it, describe} from "mocha";
import {Calculator} from "../src/calculator";
describe("Calculator", () => {
it("Should be able to add two numbers", () => {
const calculator = new Calculator();
const expected = 10;
const actual = calculator.add(5,5);
expect(actual).to.eq(expected);
});
it("Should be able to divide two numbers",() => {
const calculator = new Calculator();
const expected = 5;
const actual = calculator.div(10,2);
expect(actual).to.eq(expected);
});
});
The calculator.test.ts file declares the following elements:
- A test fixture or suite using the describe function from mocha. A test fixture is a fixed state of a set of objects used as a baseline for running tests. The purpose of a test fixture is to ensure that there is a well known and static environment in which tests are run so that results are repeatable.
- Two test cases using the it function from mocha. A test case is a set of conditions or variables under which a tester will determine whether a system under test satisfies requirements or works correctly. The process of developing test cases can also help find problems in the requirements or design of an application.
- Two test assertions using the expect function from chai. An assertion is a boolean expression at a specific point in a program which will be true unless there is a bug in the program. A test assertion is defined as an expression, which encapsulates some testable logic specified about a target under test.
As we can see in the preceding unit tests examples, we can invoke the method or function that we wish to test, and we then compare the result of the method or function with our expected result given a set of known arguments.
We can execute our tests using the npm script commands that we defined previously:
npm test
After a couple of seconds we should be able to see the results in Bash:
Writing pure functions and isolating components
The previous examples are very simple because the add and div methods in the Calculator class are pure functions. A pure function is a function that generates a result using only the arguments that are passed into the function. A pure function will never access variables that have been declared outside of the function and have not been passed as one of its arguments. Additionally,a pure function never modifies the arguments that are passed into it.
The following function is not a pure function:
function isIndexPage(){
return window.location.pathname === "/";
}
The above function is not pure because it access the window object which is a global variable and it is not passed to the function as an argument. Writing a unit tests for a function that is not pure is very difficult or even not possible. For example, to write a unit test for the above function we would have to replace the window global object with a mock or fake version of it. Because the window object is hard-coded within the body of the isIndexPage function, it is very hard to replace the window global object with a mock or fake version of it. We can avoid this problem by writing our code in a way that is easy to tests. For example, the following function is a pure function:
function isIndexPage(pathname: string){
return window.location.pathname ==="/";
}
The above function is very easy to test because we can pass the real window object in the application:
isIndexPage(window.location.pathname);
The nice thing is that we can also replace the window object with a fake version in our tests:
it ("Should be able to identify the homepage", () => {
expect(isIndexPage("/")).to.eq(true);
expect(isIndexPage("/something")).to.eq(false);
expect(isIndexPage("/something/somethingElse")).to.eq(false);
});
Writing the function as a pure function allows us to replace the dependencies of the isIndexPage function for mock versions in our tests. This allows us to test the isIndexPage function in isolation from its dependencies. Isolating components is the most fundamental skill required to write good unit tests.
Working with factory functions
In the preceding section, we transformed the isIndexPage into a pure function by passing an additional argument to it. However,sometimes there are some restrictions, which may prevent us from being able to add additional arguments to a function when we wish to do so. For example, the Expres.js HTTP GET endpoints are declared as follows:
router.get("/movies", (req,res) => {
const movies = await movieRepository.find();
res.json(movies).send();
});
The preceding code snippet passes a path and an anonymous function to the get method in a router instance. We can re-write the preceding code snippet using a named function instead:
const httpGetMoviesHandler = (req,res) => {
const movies = await movieRepository.find();
res.json(movies).send();
};
router.get("/movies",httpGetMoviesHandler);
Resume of command for a new project
tsc --init
npm init --y
npm install --save-dev @types/node
vi tsconfig.json:
"lib": [ "es6" ]
npm install --save-dev @types/express
npm install --save @types/body-parser
npm install typeorm
npm install pg
npm install reflect-metadata
vi tsconfig.json:
"lib": ["es6", "dom"],
// ...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
npm install joi
sudo chmod 777 /home/adelo/.npm/_cacache/index-v5/2e/0f/9887d5e6560e9c8ef7a548d8f9b6106d3ce109ffb20ab3aeac875381f424
npm install @types/joi
sudo npm install mocha chai @types/mocha @types/chai nyc typescript ts-node
vi package.json:
"scripts": {
"test": "nyc --clean --all --require ts-node/register --require reflect-metadata/Reflect --extension .ts --mocha --timeout 5000 **/*.test.ts"
},
Project - Implementing a REST API with TypeScript and Node.js
Simplified version of the Reddi website: https://www.reddit.com/
Descripción del proyecto: https://docs.google.com/document/d/1kV9n057BnuJ_ToU7sZ9ZKqo9rXakYp_7KdqkpETnefo/edit#
Please note that this CA will be continued by a second CA at the end of the second semester. The second CA will consist of a React frontend application and will use the HTTP REST API implemented as part of this CA.
Proyecto de Remo muy parecido:
https://github.com/WolkSoftware/public_demos/tree/master/events/dublin_typescript_meetup/2018_AUG
Setting up the project
(5%, Marks)
In this project, we are going to implement the REST API for a simplified version of the website Reddit. The user will be able to create an account/ login and post links. Other users will then be able to upvote and downvote the links.
The application should be implemented using the technologies that we used in class: TypeScript, Node.js, Express.js, TypeORM, and Postgres.
To complete this practice you are going to need to install Node.js, TypeScript, and ts-node. You will also need to create a package.json file and a tsconfig.json file. You are going to need to change the default configuration in the tsconfig.json as we have been doing in class.
Please remember to refer to the guides shared in Moodle if you need additional help:
- PART I: Setting up a Node.js + TypeScript development environment
- PART II: Express/Node.js basics with TypeScript
- PART III: Getting started with TypeORM, Postgres and Docker
- PART IV: Authentication, server-side validation, and the model-view-controller (MVC) design pattern
- PART V: Writing automated tests and debugging our Node.js applications
You project directory architecture should look as follows:
├───node_modules
│ └─── ...
├───package.json
├───tsconfig.json
├───app.ts
├───index.ts
├───db.ts
│ ├───backend
│ │ ├───controllers
│ │ ├───entities
│ │ ├───middleware
│ │ └───repositories
│ └───frontend
│ └─── client.ts
└───test
└───controllers
.
├── 'node_modules'
│ └── ...
├── tsconfig.json
├── package.json
├── package-lock.json
├── nodemon.json
├── app.ts
├── db.ts
├── 'test'
│ └── controllers
└── 'src'
└── 'backend'
├── 'repositories'
│ ├── vote_repository.ts
│ ├── users_repository.ts
│ ├── link_repository.ts
│ └── comment_repository.ts
├── 'entities'
│ ├── vote.ts
│ ├── comment.ts
│ ├── link.ts
│ └── users.ts
├── 'controllers'
│ ├── users_controller.ts
│ ├── link_controller.ts
│ ├── comment_controller.ts
│ └── auth_controller.ts
└── 'middleware'
└── auth_middleware.ts
Entities
- Create a file named: /src/backend/entities/link.ts
- This file should define the Link entity. It should contain an ID, a reference to the User an URL and a title.
- Create a file named: /src/backend/entities/user.ts
- This file should define the User entity and it must contain an ID, an email and a password.
- Create a file named /src/backend/entities/vote.ts
- This file should define the Vote entity and it should contain an ID, a reference to the user, a reference to the Link and a boolean flag that indicates if the vote is positive or negative.
- Create a file named /src/backend/entities/comment.ts
- This file should define the Comment entity and it should contain an ID, a reference to the user, a reference to the Link and a text field for the comment content.
Repositories
You must implement 4 repositories (one for each of the previously defined entities) under the /src/backend/repositories/ directory. The recommended database for this assignment is Postgres.
Controllers
(45%, 45 Marks)
We are going to need 4 controllers. Each endpoint has different requirements but all endpoints share the following set of common requirements:
- In all your controllers, the inputs of each endpoint should be validated and an error HTTP 400 (Bad Request) should be returned if the inputs are invalid.
- When a request tries to access a private endpoint without a known identity, an error HTTP 401 (Unauthorized) should be returned.
- When a user doesn't have permissions to perform an operation, an error HTTP 403 (Forbidden) should be returned.
- If an exception takes place an error HTTP 500 (Internal Server Error) should be returned..
Links controller
Create a new controller under the /src/backend/controllers/links_controller.ts directory. The links controller requires the following endpoints:
HTTP Method | URL | Description | Is public |
---|---|---|---|
/api/v1/links | GET | Returns all links | Yes |
/api/v1/links/:id | GET | Returns a link and its comments | Yes |
/api/v1/links | POST | Creates a new link | No |
/api/v1/links/:id | DELETE | Deletes a link | No |
/api/v1/links/:id/upvote | POST | Upvotes link | No |
/api/v1/links/:id/downvote | POST | Downvote slink | No |
- GET /api/v1/links is public and takes no arguments.
- POST /api/v1/links requires user authentication and takes a link in the request body. It should return the new link.
- DELETE /api/v1/links/:id requires user authentication and takes the id of a link via the request URL. A user should not be able to delete a link if he is not the owner of the link.
- POST /api/v1/links/:id/upvote requires user authentication and takes the id of a link via the request URL. A user should not be able to vote the same link multiple times.
- POST /api/v1/links/:id/downvote requires user authentication and takes the id of a link via the request URL. A user should not be able to vote the same link multiple times.
Auth controller
Create a new controller under the /src/backend/controllers/auth_controller.ts directory. The auth controller requires the following endpoints:
HTTP Method | URL | Description | Is public |
---|---|---|---|
/api/v1/auth/login | POST | Returns an auth token | Yes |
- POST /api/v1/auth/login is public and takes the user email and password as JSON in the request body. It returns a JWT token as a response.
User controller
Create a new controller under the /src/backend/controllers/users_controller.ts directory. The user controller requires the following endpoints:
HTTP Method | URL | Description | Is public |
---|---|---|---|
/api/v1/users | POST | Creates a new user account | Yes |
/api/v1/users/:id | GET | Returns a user with all its activity (links and comments) | Yes |
- POST /api/v1/users is public and takes the user email and password as JSON in the request body. It returns the new user as a response ( res.json(user); res.json(user).send() ). An error 400 should be returned if the user email is already used by another account.
- GET /api/v1/users/:id it is public and takes the user ID via the URL. It should return 404 if the user is not found. If the user is found it should return not just the user but also its links and comments.
Comment controller
Create a new controller under the /src/backend/controllers/comment_controller.ts directory. The comment controller requires the following endpoints:
HTTP Method | URL | Description | Is public |
---|---|---|---|
/api/v1/comments | POST | Creates a new comment | No |
/api/v1/comments/:id | PATCH | Updates the content of the comment (falta el input validation) | No |
/api/v1/comments/:id | DELETE | Deletes a comment | No |
- POST /api/v1/comments it is private and allows us to create a new comment by sending it in the request body.
- PATCH /api/v1/comments/:id it is private and allows us to edit an existing comment by its ID. The updated content will be sent in the request body. Users should not be able to edit comments that they don't own. An error 400 should be thrown if the user is not the owner. An error 404 should be thrown if the comment is not found.
- DELETE /api/v1/comments/:id it is private and allows us to delete an existing comment by its ID. Users should not be able to delete comments that they don't own. An error 400 should be thrown if the user is not the owner. An error 404 should be thrown if the comment is not found.
Security
(20%, 20 Marks)
You must implement user authentication using JWT tokens as we have explained during the lectures of this module. The endpoint flagged as "private" must be protected by a middleware that uses JWT tokens. Please implement the JWT tokens using the "jsonwebtoken" library.
You are going to need to define a middleware named "authMiddleware" in a file named "auth_middleware.ts" under the /src/middleware directory.
Web client
(10%, 10 Marks)
You must implement a web client for one each of the endpoints previously described.
Automated test
(20%, 20 Marks)
You must implement one integration test and one unit test:
The unit test must ensure that one of the POST methods in the links controller is correct. The controller must be tested in isolation. Please implement this test using the mocha and chai libraries.
We must ensure that one of the POST methods in the links controller is correct using an integration test. Please implement this test using the "supertest" library.