Basic Express Server setup with Typescript

Installation

mkdir server
cd server
npm init -y
tsc --init
npm install concurrently nodemon
npm install express body-parser cookie-session
npm install @types/express @types/cookie-session @types/body-parser

Project struture

mkdir build    mkdir src/routes

Edit tsconfig.json and uncomment outDir and outputDir:

"outDir": "./build",/*Redirect output structure to the directory. */   
"rootDir": "./src",/*Specify the root directory of input files. Use to control the output directory structure with --outDir. */

Configure build and run scripts

Edit package.json and add the following:

    "scripts": {
        "start:build": "tsc -w",
        "start:run": "nodemon build/index.js",
        "start": "concurrently npm:start:*"
    }

Basic Routes With Express

Create a basic login router in ./routes/loginRoutes.ts:

import { Router, Request, Response } from "express";
 interface RequestWithBody extends Request {
    body: { [key: string]: string | undefined };
 }
    const router = Router();
    router.get("/login", (req: Request, res: Response) => {
    res.send(
        `            <form method="POST">
                <div>
                    <label>Email</label>
                    <input name="email"/>
                </div>
                <div>
                    <label>Password</label>
                    <input name="password" type="password"/>
                </div>
                <button>Submit</button>
            </form>
        `    );
    });
    router.post("/login", (req: RequestWithBody, res: Response) => { 
   const { email, password } = req.body;
    if (email && password) {
        res.send(email + password);
    }
    }); 
   export { router };

Create the express server

In ./index.ts:

 import express, { Request, Response } from "express";
    import bodyParser from "body-parser";
    import { router } from "./routes/loginRoutes";
    const app = express();
    app.use(bodyParser.urlencoded({ extended: true }));
    app.use(router);
    app.listen(3000, (req, res) => { 
   console.log("Listening on port 3000"); 
   });

Wire session

In index.ts:

 import cookieSession from "cookie-session";
    app.use(cookieSession({ keys: ["mykey"] })); //the key is used for encryption purpose

Mark a user as logged in

In a route handler, to mark a user as logged in:

    //mark this person as logged in 
   req.session = { loggedIn: true }; 
   //redirect to the root route 
   res.redirect('/')

Check if a user is logged in

    if (req.session && req.session.loggedIn === true) {    }

Logout a user

  req.session = undefined;

Enable protected routes for logged in user

For this purpose we can code a middleware to check is a user is authenticated

  import { NextFunction } from "express";

    function requireAuth(req: Request, res: Response, next: NextFunction): void {
    if (req.session && req.session.loggedIn === true) { 
       next(); 
       return; 
   }
    res.status(403); 
   res.send("Not permitted"); 
   }

Set up a protected route:

    router.get('/protected', requireAuth, (req:Request, res:Response) => {
    res.send(`Welcome to protected route, logged in user`)
    })

Set up our own decorator

The goal is to have our own decorator to manage easy our router and routes handler.

  import { Request, Response } from "express"; 
   import { get, controller } from "./decorators";

    @controller("")    
class LoginController {   
 @get("/login")    
getLogin(req: Request, res: Response): void {
        res.send(
        `<form method="POST">
                <div>
                    <label>Email</label>
                    <input name="email"/>
                </div>
                <div>
                    <label>Password</label>
                    <input name="password" type="password"/> 
               </div>
                <button>Submit</button> 
           </form>`
        );
    }
    }

structure

mkdir -p src/controller/decorators
    touch src/AppRouter.ts //manage the router using as a singleton
    touch src/controller/decorators/Methods.ts //define used http method in an enum
    touch src/controller/decorators/controller.ts //contain all controller decorators
    touch src/controller/decorators/routes.ts //contain all routes decorators
    touch src/controller/decorators/index.ts //will export all routes and controller decorators
    touch src/controller/LoginController.ts //actual controller implementation

Router as a singleton

    import express from "express";

    export class AppRouter {
    private static instance: express.Router;

    static getInstance(): express.Router {
        if (!AppRouter.instance) { 
       AppRouter.instance = express.Router(); 
       return AppRouter.instance;       
 }        return AppRouter.instance;
    }
    }

Decorators

All method decorators will add information using metadata. The class decorator, which is always the last one to be executed, will get all these information and set up the express router accordingly.

Http Methods

    export enum Methods {    get = "get",    post = "post"    }

Routes decorators

import "reflect-metadata";
import { Methods } from "./Methods";
function routeBinder(method: Methods) {
  return function(path: string) {
    return function(target: any, key: string, desc: PropertyDescriptor) {
      Reflect.defineMetadata("path", path, target, key); 
     Reflect.defineMetadata("method", method, target, key); 
   };
  };
}
export const get = routeBinder(Methods.get);
export const post = routeBinder(Methods.post);

Controller decorators

import "reflect-metadata";
import { AppRouter } from "../../AppRouter";
import { Methods } from "./Methods";

export function controller(prefix: string) {
  return function(target: Function) {
    const router = AppRouter.getInstance();
    for (let key in target.prototype) {
      const routeHandler = target.prototype[key]; 
     const path = Reflect.getMetadata("path", target.prototype, key);
     const method: Methods = Reflect.getMetadata(        "method",        target.prototype,        key      ); 
     if (path) { 
       // console.log(`${prefix}${path}`, routeHandler);      
  router[method](`${prefix}${path}`, routeHandler);
      } 
   } 
 };
}