Introduction
Typescript is a great technology - which adds static typing to your javascript code, provides better intillisense support means faster development and much more. The popular client side framework Angular2 is an example of how typescript can be used to create a large project in less time.
Now you must be wondering - can we use the power of typescript to create a nodejs server ?
The answer is yes.
In this article :- we will use fortjs - a nodejs mvc framework fully compatible for typescript and next gen javascript - es6, es7.
Code
The codebase of this article can be downloaded at - Example link on github
Setup
Clone or download the typescript starter project of fortjs — https://github.com/ujjwalguptaofficial/fortjs-typescript-starter.
After you have downloaded the project. Open the console and move to project directory and do the following steps,
- run the command - npm install
- run the command - npm run start
Open the url - localhost:4000 in browser . You will see something like below,
REST
We are going to create the rest end point for entity user - which will perform the crud operations for user like adding user, deleting user, getting user, updating user.
A/c to REST,
- Adding user - should be done using http method "POST"
- Deleting user - should be done using http method "REMOVE"
- Getting user - should be done using http method "GET"
- Updating user - should be done using http method "PUT"
For creating an end point we need to create a controller. You can read about controller here - http://fortjs.info/tutorial/controller/
create a file user_controller.ts inside the contollers folder & Copy the below code inside the file,
import { Controller, textResult, DefaultWorker} from 'fortjs'
export class UserController extends Controller {
@DefaultWorker()
async default() {
return textResult('you have successfully created a user controller');
}
}
In the above code,
- We have created a class "UserController" which is extending another class Controller from fortjs.
- We have created a method default which is returning some result by using the method textResult from fortjs. textResult return http response with content-type 'text/plain'.
- We have used a decorator DefaultWorker from fortjs. A worker makes the method visible for http request so that it can be called using http request (no worker means it just a function which is only available for this class). A default worker is a worker which add the route "/" for target method. Please take a look at worker doc - http://fortjs.info/tutorial/worker/
- We have created a controller but its still unknown by fortjs & in order to use this controller ,we need to add this to routes. Open routes.ts inside folder src and add UserController to routes.
We have created a controller but its still unknown by fortjs & in order to use this controller ,we need to add this to routes. Open routes.ts inside folder src and add UserController to routes.
import {DefaultController } from "./controllers/default_controller";
import { UserController } from "./controllers/user_controller";
export const routes = [{
path: "/*",
controller: DefaultController
},{
path: "/user",
controller: UserController
}]
You can see we have added the path "/user" for UserController. It means when the path is "/user", UserController will be called.
Now open the url — localhost:4000/user. You can see the output which is returned from default method inside “UserController”.
One thing to notice here is that - codes looks very simple and nice. This is possible due to typescript & fortjs. And another fun is that - you will get intillisense support and this all make life easy for a developer :).
Service
Before moving further let’s write service code, which will help us to do crud operation.
Creating models
Create a folder “models” and then a file “user.ts” inside the folder. Paste the below code inside the file,
import { Length, Contains, IsIn, IsEmail } from "class-validator";
export class User {
id?: number;
@Length(5)
password?: string;
@Length(5)
name: string;
@IsIn(["male", "female"])
gender: string;
@Length(10, 100)
address: string;
@IsEmail()
emailId: string;
constructor(user: any) {
this.id = Number(user.id);
this.name = user.name;
this.gender = user.gender;
this.address = user.address;
this.emailId = user.emailId;
this.password = user.password;
}
}
I am using a npm plugin - "class-validator" to validate the model. This model “user” will be used by service and controller for transfer of data.
Create a folder “services” and then a file “ user_service.ts” inside the folder. Paste the below code inside the file,
import { User } from "../models/user";
interface IStore {
users: User[];
}
const store: IStore = {
users: [{
id: 1,
name: "ujjwal gupta",
address: "Bengaluru india",
emailId: "admin-ujjwal@imail.com",
gender: "male",
password: "admin"
}]
}
export class UserService {
getUsers() {
return store.users;
}
addUser(user: User) {
const lastUser = store.users[store.users.length - 1];
user.id = lastUser == null ? 1 : lastUser.id + 1;
store.users.push(user);
return user;
}
updateUser(user: User) {
const existingUser = store.users.find(qry => qry.id === user.id);
if (existingUser != null) {
existingUser.name = user.name;
existingUser.address = user.address;
existingUser.gender = user.gender;
existingUser.emailId = user.emailId;
return true;
}
return false;
}
getUser(id: number) {
return store.users.find(user => user.id === id);
}
removeUser(id: number) {
const index = store.users.findIndex(user => user.id === id);
store.users.splice(index, 1);
}
}
In the above code - we have created a dummy service. It contains a variable store which contains collection of users and the method inside the service do operation like — add, update, delete, get on that store.
GET
We are going to create an end point for getting user.
Let’s rename the default methods to “getUsers” which will return all users. Replace the code inside user_controller.ts by below code,
import { Controller, jsonResult, DefaultWorker} from 'fortjs'
export class UserController extends Controller {
@DefaultWorker()
async getUsers() {
const service = new UserService();
return jsonResult(service.getUsers());
}
}
As you can see - we are using DefaultWorker since its make the method visible for http request and add route "/" with http method "GET". So all these things using one decorator.
Let's try this using http client -
POST
We need to create a method which will add the user and only work for http method "POST". So now “UserController” looks like this,
import { Controller, jsonResult, DefaultWorker, HTTP_METHOD, HTTP_STATUS_CODE, Worker, Route } from 'fortjs'
export class UserController extends Controller {
@DefaultWorker()
async getUsers() {
const service = new UserService();
return jsonResult(service.getUsers());
}
@Worker([HTTP_METHOD.Post])
@Route("/")
async addUser() {
const user = {
name: this.body.name,
gender: this.body.gender,
address: this.body.address,
emailId: this.body.emailId,
password: this.body.password
};
const service = new UserService();
const newUser = service.addUser(user);
return jsonResult(newUser, HTTP_STATUS_CODE.Created);
}
}
In the above code,
- We have created a method "addUser" & added a decorator “Route” with parameter “/” which will add the route to method "addUser". This means - method “addUser” will be called when url will be :- localhost:4000/user/.
- In order to make this method visible for http request - we are using decorator “Worker”. The parameter “HTTP_METHOD.Post” makes the method only work when the request method will be POST.
- The method addUser -takes data from body (post data) and add the user to store by calling service. After the successful addition , it returns the added user with http code - 201 (Resource Created).
In summary - we have created a method "addUser" which sole purpose is to add user. It only works for http method post & route "/".
You can test this by sending a post request to url - "localhost:4000/user/" with user model value as body of the request.
So we have successfully created the POST end point. But one thing to note here is that - we are not doing any validation for the user. It might be that invalid data is supplied in post request.
We can write code inside the method “addUser” to validate or write a seperate method inside a controller (like validateUser) for validation.
Let's add the validation code,
import { Controller, jsonResult, DefaultWorker, HTTP_METHOD, HTTP_STATUS_CODE, Worker, Route } from 'fortjs'
import { User } from '../models/user';
import { validate } from "class-validator";
export class UserController extends Controller {
@DefaultWorker()
async getUsers() {
const service = new UserService();
return jsonResult(service.getUsers());
}
@Worker([HTTP_METHOD.Post])
@Route("/")
async addUser() {
const user = {
name: this.body.name,
gender: this.body.gender,
address: this.body.address,
emailId: this.body.emailId,
password: this.body.password
}
as User;
const errorMsg = await this.validateUser(user);
if (errorMsg == null) {
const service = new UserService();
const newUser = service.addUser(user);
return jsonResult(newUser, HTTP_STATUS_CODE.Created);
} else {
return textResult(errMessage, HTTP_STATUS_CODE.BadRequest);
}
}
async validateUser(user: User) {
const errors = await validate('User', user);
if (errors.length === 0) {
return null;
} else {
const error = errors[0];
const constraint = Object.keys(error.constraints)[0];
const errMessage = error.constraints[constraint];
return errMessage;
}
}
}
Ok, so we have added the code to validation and it will work as expected but dont you think - our code looks little polluted and with the time it will look much pollute.
FortJs provides components for validation & any extra work, so that your code looks much cleaner and easy to manage.
FortJs says - "A worker should only have code related to its main purpose and extra code should be written into components."
There are three components of fortjs -
- Wall - Used at app level
- Shield - Used at controller level
- Guard - Used at worker level
Let's implement the above validation using components - since we are doing operation on worker, we need to use Guard component.
Guard
Create a folder “guards” and a file “ model_user_guard.ts” inside the folder. Write the below code inside the file,
import { Guard, HttpResult, MIME_TYPE, HTTP_STATUS_CODE, textResult } from "fortjs";
import { User } from "../models/user";
import { validate } from "class-validator";
export class ModelUserGuard extends Guard {
async check() {
const user: User = new User(this.body);
// here i am using a plugin to validate but you can write your own code too.
const errors = await validate('User', user);
if (errors.length === 0) {
// pass this to method, so that they dont need to parse again
this.data.user = user;
return null;
}
else {
const error = errors[0];
const constraint = Object.keys(error.constraints)[0];
const errMessage = error.constraints[constraint];
return textResult(errMessage, HTTP_STATUS_CODE.BadRequest);
}
}
}
In the above code,
- We are writing code inside the check method which is part of guard lifecycle. We are validating the user inside it.
- If user is valid - then we are passing the user by using "data" property and returning null. Retuning null means guard has allowed this request and the worker should be called.
- If user is not valid - we are returning the error message as text response with http code- "badrequest". We are retuning textResult which means the fortjs will consider this as response and worker wont be called.
Now we need to add this guard to method “addUser”,
@Guards([ModelUserGuard])
@Worker([HTTP_METHOD.Post])
@Route("/")
async addUser() {
const user: User = this.data.user;
const service = new UserService();
return jsonResult(service.addUser(user), HTTP_STATUS_CODE.Created);
}
In the above code,
- I have added the guard - “ModelUserGuard” using the decorator - Guards .
- With the guard in process, we dont need to parse the data from body anymore inside worker, we are reading it from this.data which we are passing from "ModelUserGuard" .
- The method “addUser” will be only called when Guard allow means if all data is valid.
You can see that our worker method looks very light after using component.
PUT
Now we need to create a method which will update the user and will only work for http method — “PUT”.
Let’s add another method - “updateUser” with route “/” , guard — “ModelUserGuard” (for validation of user) and most important - worker with http method — “PUT”
@Worker([HTTP_METHOD.Put])
@Guards([ModelUserGuard])
@Route("/")
async updateUser() {
const user: User = this.data.user;
const service = new UserService();
const userUpdated = service.updateUser(user);
if (userUpdated === true) {
return textResult("user updated");
}
else {
return textResult("invalid user");
}
}
The above code is very simple, just calling the service code to update the user. But one important thing to notice is that we have reutilized the guard - “ModelUserGuard” and it makes our code very clean.
So we are done with,
- GET - Returns all users
- POST - add users
- PUT - update user
Currently the GET request returns all the users but what if we want to get only one user.
Let’s see : how to do it,
We have created a method “getUsers” for returning all users. Now let’s create another method “getUser” which will return only one user.
@Worker([HTTP_METHOD.Get])
@Route("/{id}")
async getUser() {
const userId = Number(this.param.id);
const service = new UserService();
const user = service.getUser(userId);
if (user == null) {
return textResult("invalid id");
}
return jsonResult(user);
}
In the above code - we are using a place holder in route. Now “getUser” will be called when url will be something like localhost:4000/user/1 The placeholder value is being consumed by using "this.param" .
REMOVE
We will use the same concept as get,
@Worker([HTTP_METHOD.Delete])
@Route("/{id}")
async removeUser() {
const userId = Number(this.param.id);
const service = new UserService();
const user = service.getUser(userId);
if (user != null) {
service.removeUser(userId);
return textResult("user deleted");
}
else {
return textResult("invalid user");
}
}
In the above code - we are just calling the service to remove the user after getting the id from route.
Finally, we have successfully created a rest end point for user.
Summary
TypeScript makes the development very faster with static typing & intillisense support. On the other hand : fortjs - help you to write the server code which is very clean, modular & secure.
Top comments (0)