In one of our Angular courses we are currently using a Node Express server to provide mock data to an Angular application.
As Deno has been released with the official version 1.0 (mid May 2020), I decided to experiment it and write a new web server for the course. The final layout looks like the screenshot below (as you can see nothing fancy from the layout perspective):
This article is a step-by-step, practical guide focused on creating a Rest API, with full CRUD actions, for an Angular application. I will not cover too many details about Deno though, as it would make the post way too long, and there are already plenty of good introductions to Deno already.
Below are the topics that we will cover, feel free to follow along or jump directly to the topic of interest if you prefer:
Deno:
Angular:
Repo
What is Deno
Deno has been created by Ryan Dahl, the same creator of Node.js.
Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.
If you are already familiar with Node, then Deno is able to do exactly the same things, but faster.
Deno can be seen as a way to rewamp Node.js, solving different aspects that the same Ryan considered his "regrets".
Below is his talk at the JSConf EU 2018, where he exposes these points:
Install Deno
We can choose to use a package manager or execute directly a command in the shell.
Install via command
With Shell (Mac):
curl -fsSL https://deno.land/x/install/install.sh | sh
With PowerShell (Windows):
iwr https://deno.land/x/install/install.ps1 -useb | iex
Install via Package Manager
With Homebrew (Mac):
brew install deno
With Chocolatey (Windows):
choco install deno
After Deno is downloaded and setup locally, run deno --version
in the shell to verify the installation. We should get a similar output:
$ deno --version
deno 1.0.3
v8 8.4.300
typescript 3.9.2
If we want an overview about the available commands, we can invoke the instruction deno --help
, showing all the available sub-commands.
We can even get further details for each single command simply appending the --help
flag, like: deno run --help
Available modules
Deno provides a list of standard modules, reviewed by the core team and guaranteed to work with the specific Deno version. These standard modules are hosted at https://deno.land/std and provide functionalities for most of the basic tasks like: uuid generation, http calls and file system access, for instance.
Aside these, deno.land website also provides a public hosting service for third party modules that are compatible with Deno at deno.land/x.
We can search among an exhaustive collection of modules.
Create a Server
Now that everything is in place, let's start writing some code. Define a root folder for your server:
mkdir webServer && cd webServer
Server.ts
Create a server.ts
file.
💡 Note: we can use plain JavaScript instead of typescript, but we would lose many of the benefits that typescript offers. Moreover, being Deno written on top of Rust and Typescript, it compiles directly .ts files for us.
Use the standard http module
To create an HTTP server we could import the server.ts
file from the http
standard module:
import { serve } from "https://deno.land/std@0.125.0/http/server.ts";
const server_port = 5400;
function req_handler(req: Request): Response {
console.log("\nReceived a request...\n");
const body = JSON.stringify({ message: "I am a DENO server 🦕" });
return new Response(body, {
status: 200,
headers: {
"content-type": "application/json; charset=utf-8",
},
});
}
serve(req_handler, { port: server_port})
console.log("Listening on PORT: ", server_port);
💡 Note: you might have noticed that we import from a url here rather than from a local path.
This is a new concept in Deno, we don't need to have the packages already installed locally. We can import their latest version and cache it, making it available even while offline. Deno uses ES Modules to import packages, where from Node uses npm. This translates in the absence of thenode_modules
folder andpackage.json
file (we don't have any trace of them in our webServer folder). With Deno we directly import the packages we want through its url.
Use third party module
Alternatively we can opt for oak
, a middleware framework for Deno's http server, including a router middleware. This middleware framework is inspired by Koa, therefore already familiar to many Node.js developers. For our mock server I decided to use oak.
import { Application } from "https://deno.land/x/oak/mod.ts";
import { oakCors } from "https://deno.land/x/cors/mod.ts";
import router from "./src/routes.ts";
const port = 8280;
const app = new Application();
app.use(oakCors());
app.use(router.routes());
app.use(router.allowedMethods());
app.addEventListener("listen", ({ hostname, port, secure }) => {
console.log(`--- Listening on: ${secure ? "https://" : "http://"}${
hostname ?? "localhost"
}:${port}`
);
});
await app.listen({ port });
If you already used Express the code above should be already very familiar. After creating an instance of the Application
class, we can stack multiple middleware using the use()
method and then activate the server (listen()
method), waiting for incoming requests.
CORS
We can define CORS for our application otherwise we would get a client-side error every time we try to reach our server from the Angular app. Deno provides a cors module (https://deno.land/x/cors/mod.ts
) with default settings that already capture many common cases. We can enable CORS with the following call:
app.use(oakCors());
The default configuration, hence without parameters like in the snippet above, translates in the following set of options:
{
"origin": "*",
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false,
"optionsSuccessStatus": 204
}
Routes.ts
As our mock server is pretty simple, I decided to create just a folder src
to host all the business logic and keep it separated from the server.ts
file.
The routes file contains all the endpoints that we want to expose to the Angular client and in our case implement the classic CRUD operations.
import { Router } from "https://deno.land/x/oak/mod.ts";
import {
getAllEmployees, getEmployeeById, updateEmployee, addEmployee, deleteEmployee
} from "./employeeApis.ts";
const router = new Router();
router.get("/employees", getAllEmployees)
.get("/employees/:id", getEmployeeById)
.put("/employees/:id", updateEmployee)
.post("/employees", addEmployee)
.delete("/employees/:id", deleteEmployee);
export default router;
Employee.ts
We need to define a generic model for our domain. Here we design an Employee
object with some static data and no database storage, as it would be beyond the course scope, focusing on Angular and client side development only.
export interface Employee {
id: number;
firstname: string;
lastname: string;
email?: string;
}
export const EmployeeData: Employee[] = [
{ id: 1, firstname: 'Larry', lastname: 'Potter', email: 'larry.potter@hotmail.com' },
{ id: 2, firstname: 'Mara', lastname: 'Croft', email: 'mara.croft@gmail.com' },
{ id: 3, firstname: 'Thomas', lastname: 'Müller', email: 'thomas123@gmail.com' },
{ id: 5, firstname: 'Karl', lastname: 'Fritz', email: 'Karl_great@microsoft.com' },
{ id: 6, firstname: 'Paolo', lastname: 'Rossi' }
];
EmployeeApis.ts
In this file we implement the real logic behind each endpoint. Any data mutation will affect the local data structure EmployeeData, seen above.
The code is very simple and self-explanatory therefore I won't go in detail about it.
import { EmployeeData, Employee } from './employee.ts';
// Returns all available employees
export const getAllEmployees = ({ response }: { response: any }) => {
response.body = EmployeeData;
};
// Returns one employee by its Id or 404 if not found
export const getEmployeeById = ({ params, response }: { params: { id: string }; response: any }) => {
const selectedEmployee: Employee | undefined = EmployeeData.find((employee) =>
employee.id === +params.id
);
if (selectedEmployee) {
response.status = 200;
response.body = selectedEmployee;
}
else {
response.status = 404;
response.body = [];
}
};
// Add a new employee to the list
export const addEmployee = async (
{ request, response }: { request: any; response: any },
) => {
if (!request.hasBody) {
response.status = 400;
} else {
const newEmployee: Employee = await request.body();
newEmployee.id = getNextEmployeeId();
EmployeeData.push(newEmployee);
response.status = 201;
}
};
//Provides the next number to be used as employee Id
function getNextEmployeeId(): number {
let maxId = 1;
EmployeeData.forEach(p => {
maxId = Math.max(p.id, maxId);
});
return maxId + 1;
}
// Removes an employee by its Id or 404 if not found
export const deleteEmployee = (
{ params, response }: { params: { id: string }; response: any },
) => {
const targetId = +params.id;
const newEmployeeList = EmployeeData.filter(x => x.id !== targetId);
if (newEmployeeList.length < EmployeeData.length) {
replaceCollection(EmployeeData, newEmployeeList);
response.status = 200;
} else {
response.status = 404;
}
};
// Updates en existing employee
export const updateEmployee = async (
{ params, request, response }: {
params: { id: string };
request: any;
response: any;
},
) => {
const targetId = +params.id;
let employeeToUpdate: Employee | undefined = EmployeeData.find((employee) =>
employee.id === targetId
);
if (employeeToUpdate) {
const body = await request.body();
const newEmployeeData: Employee = body.value;
let updatedData = EmployeeData.map((e: Employee) => {
return e.id === targetId ? { ...e, ...newEmployeeData } : e;
});
replaceCollection(EmployeeData, updatedData);
response.status = 200;
} else {
response.status = 404;
}
};
// Replaces the employee data structure with a new collection
function replaceCollection(originalData: Employee[], newData: Employee[]) {
originalData.splice(0, originalData.length);
originalData.push(...newData);
}
Start the server
Now that we created all the needed files, it is time to start the server. Execute the following command in the shell from the path hosting your server file:
deno run --allow-net server.ts
⚠️ Note: if you didn't add the Deno install root ($HOME/.deno/bin) to your environment variables, then you have to prepend the file path to server.ts
By running the command, different modules are downloaded, but no folder inside our solution is created for them.
Compile file:///.../server.ts
Download https://deno.land/x/oak/mod.ts
Download https://deno.land/x/oak/application.ts
Download https://deno.land/x/oak/context.ts
Download https://deno.land/x/oak/cookies.ts
Download https://deno.land/x/oak/httpError.ts
Download https://deno.land/x/oak/middleware.ts
Download https://deno.land/x/oak/request.ts
Download https://deno.land/x/oak/response.ts
Download https://deno.land/x/oak/router.ts
Download https://deno.land/x/oak/send.ts
Download https://deno.land/x/oak/types.ts
Download https://deno.land/x/oak/deps.ts
Download https://deno.land/x/oak/keyStack.ts
Download https://deno.land/x/oak/tssCompare.ts
Download https://deno.land/std@v1.0.0-rc1/http/server.ts
...
These modules are cached from now on and we do not need to download them again, unless we explicitly want to, using the --reload
option, for instance. By default, the cached modules are stored in Deno's base directory: $HOME/.deno (DENO_DIR), but we can change this location if we need, typically in the case of a production environment.
DENO_DIR
contains the following files and directories:
💡 Note: in Deno, you need to give explicit permissions before running a program. In the command above we had to grant network access with the option:
--allow-net
If we omit this option, we get the following error after downloading all the modules:
error: Uncaught PermissionDenied: network access to "127.0.0.1:8280", run again with the --allow-net flag
at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
at Object.sendSync ($deno$/ops/dispatch_json.ts:72:10)
at Object.listen ($deno$/ops/net.ts:51:10)
at listen ($deno$/net.ts:164:18)
at Application.serve (server.ts:261:20)
at Application.listen (application.ts:106:31)
at server.ts:18:11
And that was all we need to create a simple http server to use as a mock for our client application. Let's create now an Angular project that uses our REST APIs.
Debugging
Deno supports V8 Inspector Protocol. We can debug Deno programs with Chrome DevTools or other clients that support the protocol.
As most probably we are using Visual Code to implement our Angular application, let's see how we can debug the Deno server directly from our IDE. An official plugin is currently under construction, but for the time being we can create a launch.json
file:
{
"version": "0.2.0",
"configurations": [
{
"name": "Deno",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "deno",
"runtimeArgs": ["run", "--inspect-brk", "-A", "server.ts"],
"port": 9229
}
]
}
💡 Note: if you named your script file differently than
server.ts
you have to adapt the last item of "runtimeArgs" accordingly.
With the configuration above, VS Code debugger will run at: 127.0.0.1:9229
and intercept all the breakpoints we set.
More about Deno
If you are interested in knowing more about Deno, I recommend the official blog post about the v 1.0 release.
Keep also an eye on the Deno Cheat Sheet as it is a great resource to have always an overview about all available commands.
Create an Angular service
For the Angular part, I will describe only the http service calling our REST server. All the code is available on the Github repo anyway and you can download the whole project.
If you don't have already an existing Angular application and you need instructions on how to create one, have a look at my post about it.
EmployeeService.ts
Thanks to schematics, generate files in Angular is very easy:
ng g service employee
This command creates the EmployeeService.ts
and its unit test file. In the service, we define the methods implementing the CRUD operations and that will call the endpoints of the Deno server that we implemented before.
import { Employee } from './../model/employee.model';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable , of , throwError as _throw } from 'rxjs';
import { catchError, delay, map } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
@Injectable()
export class EmployeeService {
constructor(private http: HttpClient) { }
getEmployees(): Observable<Employee[]> {
return this.http
.get<Employee[]>(`${environment.apiBaseUrl}/employees`)
.pipe(catchError((error: any) => _throw(error)));
}
getEmployee(id: number): Observable<Employee> {
return this.http
.get<Employee>(`${environment.apiBaseUrl}/employees/${id}`)
.pipe(catchError((error: any) => _throw(error)));
}
createEmployee(payload: Employee): Observable<Employee> {
return this.http
.post<Employee>(`${environment.apiBaseUrl}/employees`, payload)
.pipe(catchError((error: any) => _throw(error)));
}
updateEmployee(payload: Employee): Observable<Employee> {
return this.http
.put<Employee>(`${environment.apiBaseUrl}/employees/${payload.id}`, payload)
.pipe(catchError((error: any) => _throw(error)));
}
removeEmployee(payload: Employee): Observable<any> {
return this.http
.delete<any>(`${environment.apiBaseUrl}/employees/${payload.id}`)
.pipe(catchError((error: any) => _throw(error)));
}
}
Environment.ts
In the file environment.ts
we can save the base url for the server and eventually other configuration keys. environment.prod.ts
, reserved for prod builds, typically has keys with different values, to target the production server instead of the staging one.
export const environment = {
production: false,
apiBaseUrl: 'http://localhost:8280'
};
Conclusion
As we saw, it is very easy to create a web server with Deno and use it as a mock for our client application.
This architecture is very convenient because it allows to decouple our web app from the server mocks. We can execute real network calls from our client without need to apply any change to our Angular app before deploying it to production.
Github Repo
The sample code (Angular and Deno server) is available on Github: https://github.com/pacoita/deno-api-mock
Top comments (3)
Thanks for the article! I am curious there are so much information and many articles about deno these days, do you think there are good arguments to switch from nodejs or is it only the new trend technology?. Thanks!
I would not switch my "production level" app to Deno yet. It is still too young to be sure you can bet on it. However I would start using Deno for my smaller projects or POCs instead of Nodejs, as Deno filled many of the tech gaps still affecting nodejs (the author "regrets"). I did not run benchmarks, but for the experience I made with Deno it looks a very promising technology.
Thanks for the article! it looks good Check this article too how to build a CLI tool using Deno
loginradius.com/engineering/blog/b...