Node Express Typescript 2024

From Logic Wiki
Jump to: navigation, search

Setup.

terminal :

npm init -y
npm i --save express dotenv
npm i -D typescript @types/node  @types/express nodemon ts-node
npx tsc --init


tsconfig.json

"rootDir": "./src", 
"outDir": "./dist",

package.json ->

{
  "name": "logicmade_api",
  "version": "1.0.0",
  "description": "API for many things needed",
  "main": "index.js",
  "scripts": {
    "build": "npx tsc",
    "start": "node dist/index.js",
    "dev": "nodemon src/index.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.28",
    "nodemon": "^3.1.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.4.2"
  },
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.18.3"
  }
}

.env file

PORT=3000

./src/index.ts ->

import express, {Express, Request, Response } from "express"
import dotenv from "dotenv"

dotenv.config()

const app: Express  = express()
const port = 3000

console.log("env : ", process.env.PORT )
app.get("/", (req:Request, res:Response) => {
    res.send("OK")
})

app.listen(port, ()=> {
    console.log(`[server]: Server is running at http://localhost:${port}`)
})

Vitest

npm i -D vitest 
npm i -D @vitest/ui

package.json ->

"scripts": {
...
    "test": "vitest",
    "test:ui": "vitest --ui",
    "coverage": "vitest run --coverage"
  },

Morgan (Logging the requests)

npm i -S morgan
npm i -D @types/morgan
npm i rotating-file-stream

/src/index.ts

import morgan from "morgan"
import  { createStream } from "rotating-file-stream"
import path from "path"
...
import Router from "./routes"
...
var accessLogStream = createStream('access.log', {
    interval: '1d', // rotate daily
    path: path.join(__dirname, '../log')
})
...
app.use(express.json());
app.use(morgan('combined', { stream: accessLogStream }));
app.use(express.static("public"));

app.use(Router);

/src/routes/index.ts

import express from "express";
import PingController from "../controllers/ping";

const router = express.Router();

router.get("/ping", async (_req, res) => {
  const controller = new PingController();
  const response = await controller.getMessage();
  return res.send(response);
});

export default router;

/src/controller/ping.ts

interface PingResponse {
    message: string;
  }
  
  export default class PingController {
    public async getMessage(): Promise<PingResponse> {
      return {
        message: "hello",
      };
    }
  }

Swagger

npm i -S tsoa swagger-ui-express
npm i -D @types/swagger-ui-express concurrently

tsconfig.json

{
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

tsoa.json (create)

{
  "entryFile": "src/index.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "spec": {
    "outputDirectory": "public",
    "specVersion": 3
  }
}

If bearer token will be used then json should be like below.

{
    "entryFile": "src/index.ts",
    "noImplicitAdditionalProperties": "throw-on-extras",
    "spec": {
      "outputDirectory": "public",
      "specVersion": 3,
      "securityDefinitions": {
        "BearerAuth": {
          "type": "http",
          "scheme": "bearer"
        }
      }
    },
    "ignore": ["**/node_modules/**"]
    
  }

For other security options see tsoa documentation. Some other samples are below

    "securityDefinitions": {
      "api_key": {
        "type": "apiKey",
        "name": "access_token",
        "in": "query"
      },
      "tsoa_auth": {
        "type": "oauth2",
        "authorizationUrl": "http://swagger.io/api/oauth/dialog",
        "flow": "implicit",
        "scopes": {
          "write:pets": "modify things",
          "read:pets": "read things"
        }
      }
    },

package.json

  "nodemonConfig": {
    "watch": [
      "src"
    ],
    "ext": "ts",
    "exec": "ts-node src/index.ts"
  },
"scripts": {
    "predev": "npm run swagger",
    "prebuild": "npm run swagger",
    "build": "npx tsc",
    "start": "node dist/index.js",
    "dev": "concurrently \"nodemon\" \"nodemon -x tsoa spec\"",
    "swagger": "tsoa spec",
    ...
  },

Usage of Concurrently : concurrently 'command1 arg' 'command2 arg'


src/index.ts

import express, { Application, Request, Response } from "express";
import morgan from "morgan";
import swaggerUi from "swagger-ui-express";

import Router from "./routes";

const PORT = process.env.PORT || 8000;

const app: Application = express();

app.use(express.json());
app.use(morgan("tiny"));
app.use(express.static("public"));

app.use(
  "/docs",
  swaggerUi.serve,
  swaggerUi.setup(undefined, {
    swaggerOptions: {
      url: "/swagger.json",
    },
  })
);

app.use(Router);

src/controllers/ping.ts

import { Get, Route } from "tsoa";

interface PingResponse {
  message: string;
}

@Route("ping")
export default class PingController {
  @Get("/")
  public async getMessage(): Promise<PingResponse> {
    return {
      message: "hello",
    };
  }
}

After making all the changes and running the server, visit http://localhost:3000/docs/ to access the APIs documentation.

Drizzle (left uncompleted)

Depending on the database select packages to install by checking Drizzle docs

Neon

Neon provides postgres db in the cloud. Check : neon official web site

Drizzle setup

npm i drizzle-orm @neondatabase/serverless
npm i -D drizzle-kit

/db/db.ts (different from original doc because of type error)

import { neon } from '@neondatabase/serverless';
import type { NeonQueryFunction } from "@neondatabase/serverless";
import { drizzle } from 'drizzle-orm/neon-http';

const sql: NeonQueryFunction<boolean, boolean> = neon(process.env.DRIZZLE_DATABASE_URL!);
const db = drizzle(sql);
export default db;

I couldn't find automatic schema updates in drizzle and swithing back to TypeORM

TypeORM

npm i typeorm pg

/src/config/config.ts

const config = {
    db:{
        sync:true,
        migration:true
    }
}
export default config

/src/db/connection.ts

import { DataSourceOptions } from "typeorm";
import dotenv from "dotenv"

dotenv.config()
const config : DataSourceOptions = {
    type: "postgres",
    host:  process.env.DB_HOST,
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME ,
    entities:[
        "src/db/entities/**/*.ts"
    ],
    migrations: [
        "src/db/migrations/**/*.ts"
    ],
    synchronize: true,
    logging: false,
    migrationsRun: false,
    extra: {
        ssl: true
      }
}
export default config

extra is needed for neon postgres sql

/src/db/dbSource.ts

import {DataSource} from "typeorm";
import connection from './connection'
import config from "../config/config"

const DbSource = new DataSource(connection)

DbSource.initialize()
    .then(() => {
        console.log("Database connected - " + connection.database)
        if(config.db.sync?.toString()=="true"){
          DbSource.synchronize().then(() => {
            console.log("Database syncronized")
            if(config.db.migration?.toString()=="true"){
              DbSource.runMigrations().then(()=>{
                console.log("Database migrations run")
              })
            }
          })
        }
}).catch(err => {
  console.log("Unable to connect to db", err);
  process.exit(1)
})
 

export default DbSource

/src/db/entities/user.entity.ts

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity("logicUser")
export class User {
    @PrimaryGeneratedColumn("uuid")
    id!: string;

    @Column()
    fullname!: string;
 
    @Column({nullable:true})
    description!: string;

    @Column({nullable:true})
    email!: string;
}

There is a similar model class


/src/repositories/user.repository.ts

import crypto from 'crypto'
import DbSource from '../db/dbSource';
import { User } from '../db/entities/user.entity';
import { RetVal } from '../models/retval.model';
import { UserModel } from '../models/user.model';
 
export const getUser = async (id:string) : Promise<RetVal> => { 
    const userRepository = DbSource.getRepository(User);
    try{ 
    const result = await userRepository.findOne({
        where:{
            id:id
        },
    })
 
    return  new RetVal(null, result)
    }catch{
        return new RetVal(Error("No record found for given id"), null) 
    }
}

export const getUsers = async () : Promise<RetVal> => { 
    const userRepository = DbSource.getRepository(User);
    try{ 
    const result = await userRepository.find({
        order:{fullname:"ASC"}

    })
 
    return  new RetVal(null, result)
    }catch{
        return new RetVal(Error("No record found for given id"), null) 
    }
}

export const addUser = async (model : UserModel) : Promise<RetVal>  => {
    const userRepository = DbSource.getRepository(User);
    const userEntity = userRepository.create({
        id:crypto.randomUUID(),
        fullname:model.fullname,
        description:model.description,
        email:model.email
    })
    try{
        const result = await userRepository.save(userEntity);
        return new RetVal(null, result)
    }catch(error){
        let message;
        if (error instanceof Error) message = "Error : " + error.message
      
        return new RetVal(Error("Error saving company"), null)
    }
}