top

TypeNexus

CI NPM Downloads NPM version typeorm@^0.3.12 express@^4.18.2

TypeNexus is a great tool for API encapsulation and management. It offers a clean and lightweight way to bundle TypeORM + Express.js functionality, helping you to build applications faster while reducing template code redundancy and type conversion work.

Installation

$ npm install typenexus

Its important to set these options in tsconfig.json file of your project:

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

Its important to set these options in package.json file of your project:

{
  "type": "module",
}

Quick start

import { TypeNexus } from 'typenexus';

(async () => {
  const app = new TypeNexus();
  await app.start();
  // Open in browser http://localhost:3000
})();

❶ Create API

./src/controller/UserController.ts

import { TypeNexus, Controller, Param, Body, DataSource } from 'typenexus';
import { Get, Post, Put, Delete, Patch, Delete, Head } from 'typenexus';
import { User } from '../entities/user.entity';

@Controller('/api/users')
export class UserController {
  @Get()          // => GET /api/users
  public async getAll(@DSource() dataSource: DataSource): Promise<User[]> {
    return dataSource.manager.find(User);
  }
  @Get('/:id')    // => GET /api/users/:id
  public async getById(@Param('id') id: string, @DSource() dataSource: DataSource): Promise<User> {
    return dataSource.manager.findOne(User, id);
  }
  @Post('/:id')   // => POST /api/users/:id
  public async modify(@Body() body: { name: string; }): Promise<{ name: string; }> {
    return { name: body.name + '~~' }
  }
  @Put('/:id')    // => PUT /api/users/:id
  public async modify(@Param('id') id: string): Promise<{ uid: string; }> {
    return { uid: id }
  }
  @Delete('/:id') // => DELETE /api/users/:id
  public async modify(@Param('id') id: string): Promise<{ uid: string; }> {
    return { uid: id }
  }
  @Patch('/:id')  // => PATCH /api/users/:id
  public async patch(): Promise<any> {
    return { id: 12 }
  }
  @Head('/:id')   // => HEAD /api/users/:id
  public async head(): Promise<{ id: number; }> {
    return { id: 12 }
  }
}

This class will register routes specified in method decorators in your server framework Express.js.

❷ Create Entity

Entity is a class that maps to a database table (or collection when using Postgres). You can create an entity by defining a new class and mark it with @Entity():

./src/entities/user.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn, CreateDateColumn } from 'typenexus';
// OR: 
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn, CreateDateColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column({ select: false })
  password: string;

  @CreateDateColumn()
  createAt: Date;
}

❸ Create Server

./src/entities/user.entity.ts

import { TypeNexus } from 'typenexus';
import { UserController } from './controller/User.js';

;(async () => {
  const app = new TypeNexus();
  // ❶ Performs connection to the database.
  await app.connect({ 
    type: 'postgres',
    host: process.env.HOST || 'localhost',
    port: 5432,
    username: process.env.DB_USER || 'postgres',
    password: process.env.DB_PASS || 'wcjiang',
    database: process.env.DB_NAME || 'typenexus-base',
    synchronize: true,
    logging: true,
    entities: ['dist/entity/*.js'],
    // OR: 
    // entities: [User],      
  });
  // ❷ 🚨 Please be sure to use it after `app.connect()`.
  app.controllers([UserController]);
  // ❸ Listen for connections.
  await app.start();

})();

Open in browser http://localhost:3000/users. You will see This action returns all users in your browser. If you open http://localhost:3000/api/users/1 you will see This action returns user data.

└── src
    ├── controller
    │   └── User.ts
    ├── entity
    │   └── User.ts
    └── index.ts

What is DataSource

Your interaction with the database is only possible once you setup a DataSource. TypeORM's DataSource holds your database connection settings and establishes initial database connection or connection pool depending on the RDBMS you use.

import { TypeNexus } from 'typenexus';
import crypto from 'crypto';
import User from './entity/User.js'

const app = new TypeNexus(3000, { .... });
await app.connect();

// You can use the DataSource example here.
// 🚨 Please be sure to use it after `app.connect()`.
const repos = app.dataSource.getRepository(User);
// Check if there is an admin account.
const adminUser = await repos.findOneBy({ username: 'wcj' });
if (!adminUser) {
  const hashPassword = crypto.createHmac('sha256', '1234').digest('hex');
  // Create an admin account.
  const user = await repos.create({
    username: 'wcj',
    name: '管理员',
    password: hashPassword,
  });
  await repos.save(user);
}

// 🚨 Please be sure to use it after `app.connect()`.
app.controllers([UserController]);
await app.start();

Use app.dataSource to get the DataSource instance.

What is DataSourceOptions

dataSourceOptions is a data source configuration you pass when you create a new DataSource instance. Different RDBMS-es have their own specific options.

import { TypeNexus, TypeNexusOptions } from 'typenexus';
const options: TypeNexusOptions = {
  dataSourceOptions: {
    type: 'postgres',
    host: process.env.POSTGRES_HOST || 'localhost',
    port: 5432,
    username: process.env.POSTGRES_USER || 'postgres',
    password: process.env.POSTGRES_PASSWORD || 'wcjiang',
    database: process.env.POSTGRES_DB || 'typenexus-base',
    synchronize: true,
    logging: true,
    entities: ['dist/entity/*.js'],
    // entities: [User], 
  },
}

;(async () => {
  const app = new TypeNexus(3000, options);
  await app.connect();
  app.controllers([UserController]);
  app.express.disable('x-powered-by');
  await app.start();
})();

It can also be passed as a parameter inside the app.connect() method:

await app.connect({ ... });

What is Entity?

Entity is a class that maps to a database table (or collection when using Postgres). You can create an entity by defining a new class and mark it with @Entity():

import { Entity, PrimaryGeneratedColumn, Column } from "typenexus"

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  firstName: string

  @Column()
  lastName: string

  @Column()
  isActive: boolean
}

This will create following database table:

+-------------+--------------+----------------------------+
|                          user                           |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| firstName   | varchar(255) |                            |
| lastName    | varchar(255) |                            |
| isActive    | boolean      |                            |
+-------------+--------------+----------------------------+

TypeNexus Options

import { DataSourceOptions } from 'typeorm';
import { OptionsUrlencoded, OptionsJson, OptionsText, Options } from 'body-parser';
import { SessionOptions } from "express-session";

export interface TypeNexusOptions {
  port?: number;
  /** Global route prefix, for example '/api'. */
  routePrefix?: string;
  /** DataSourceOptions is an interface with settings and options for specific DataSource. */
  dataSourceOptions?: DataSourceOptions;
  /** Create a session middleware */
  session?: SessionResult | SessionCallback;
  /** Node.js body parsing middleware. */
  bodyParser?: {
    /**
     * Returns middleware that parses all bodies as a string and only looks at requests where the Content-Type header matches the type option.
     */
    text?: OptionsText;
    /**
     * Returns middleware that parses all bodies as a Buffer and only looks at requests where the Content-Type header matches the type option.
     */
    raw?: Options;
    /**
     * Returns middleware that only parses json and only looks at requests where the Content-Type header matches the type option.
     */
    json?: false | OptionsJson;
    /**
     * Returns middleware that only parses urlencoded bodies and only looks at requests where the Content-Type header matches the type option
     * Used for parsing request bodies in application/x-www-form-urlencoded format.
     * @default `{extended:false}`
     */
    urlencoded?: false | OptionsUrlencoded;
  };
  /** Node.js compression middleware. The following compression codings are supported: deflate | gzip */
  compression?: false | CompressionOptions;
}

Example of parameter configuration:

new TypeNexus(3000, { routePrefix: 'api' });

More Examples

Using Request and Response objects

@Req() decorator injects you a Request object, and @Res() decorator injects you a Response object. If you have installed typings, you can use their types:

import { Controller, Req, Res, Get } from 'typeorm';
import { Response, Request }from 'express';

@Controller()
export class UserController {
  @Get('/users') // => GET /users
  getAllUsers(@Req() request: Request, @Res() response: Response) {
    return response.send('Hello response!');
  }

  @Get('/posts') // => GET /posts
  getAllPosts(@Req() request: Request, @Res() response: Response) {
    // some response functions don't return the response object,
    // so it needs to be returned explicitly
    response.redirect('/users');
    return response;
  }
}

You can use framework's request and response objects directly. If you want to handle the response by yourself, just make sure you return the response object itself from the action.

Prefix all controllers routes

If you want to prefix all your routes, e.g. /api you can use routePrefix option:

import { TypeNexus } from 'typenexus';
import { UserController } from './controller/User.js';

;(async () => {
  const app = new TypeNexus(3033);
  // 🚨 Be sure to put it in front of `app.controllers()`
  app.routePrefix = '/api'
  app.controllers([UserController]);
})();

You can also achieve the same effect by configuring routePrefix in the parameters when instantiating TypeNexus:

const app = new TypeNexus(3033, {
  routePrefix: '/api'
});

Prefix controller with base route

You can prefix all specific controller's actions with base route:

import { Controller, Get } from 'typeorm';

@Controller('/api')
export class UserController {
  @Get("/users/:id")  // => GET /api/users/12
  public async getOne() {}
  @Get("/users")      // => GET /api/users
  public async getUsers() {}
  // ...
}

Using DataSource objects

@DSource() decorator injects you a DataSource object.

import { Controller, Get, DSource, DataSource } from 'typenexus';
import { Response, Request }from 'express';
import { User } from '../entity/User.js';

@Controller('/users')
export class UserController {
  @Get() // => GET /users
  public async getUsers(@DSource() dataSource: DataSource): Promise<User[]> {
    return dataSource.manager.find(User);
  }
}

Inject request body

To inject request body, use @Body decorator:

import { Controller, Post, Body } from 'typeorm';

type UserBody = { username: string; id: number; };

@Controller()
export class UserController {
  @Post("/users") // => POST /users
  saveUser(@Body() user: UserBody) {
    // ...
  }
}

Inject request body parameters

To inject request body parameter, use @BodyParam decorator:

import { Controller, Post, BodyParam } from 'typeorm';

type UserBody = { username: string; id: number; };

@Controller()
export class UserController {
  @Post("/users") // => POST /users
  saveUser(@BodyParam("name") userName: string) {
    // ...
  }
}

Inject request header parameters

To inject request header parameter, use @HeaderParam decorator:

import { Controller, Post, HeaderParam } from 'typeorm';

@Controller()
export class UserController {
  @Post("/users")
  saveUser(@HeaderParam("authorization") token: string) {
    // ...
  }
}

If you want to inject all header parameters use @HeaderParams() decorator.

Inject query parameters

To inject query parameters, use @QueryParam decorator:

import { Controller, Get, QueryParam } from 'typeorm';

type UserBody = { username: string; id: number; };

@Controller()
export class UserController {
  @Get("/users")
  public async getUsers(@QueryParam("limit") limit: number) {
    // ....
  }
}

If you want to inject all query parameters use @QueryParams() decorator.

import { Controller, Get, QueryParams } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users")
  public async getUsers(@QueryParams() query: any) {
    // ....
  }
}

Inject routing parameters

You can use @Param decorator to inject parameters in your controller actions:

import { Controller, Get, Param } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users/:id")
  getOne(@Param("id") id: string) {}
}

If you want to inject all parameters use @Params() decorator.

To get a cookie parameter, use @CookieParam decorator:

import { Controller, Get, CookieParam, CookieParams } from 'typeorm';

@Controller()
export class UserController {
  @Get("/users")
  public async getUsers(@CookieParam("token") token: string) {
    // ....
  }
}

If you want to inject all header parameters use @CookieParams() decorator.

Inject session object

To inject a session value, use @SessionParam decorator:

@Get("/login")
savePost(@SessionParam("user") user: User, @Body() post: Post) {}

If you want to inject the main session object, use @Session() without any parameters.

@Get("/login")
savePost(@Session() session: any, @Body() post: Post) {}

Express uses express-session to handle session, so firstly you have to install it manually to use @Session decorator. Here is an example of configuring Session, and you need to create a database table entity for Session as well:

import { TypeNexus, DataSourceOptions } from 'typenexus';
import { UserController } from './controller/User.js';
import { Session } from './entity/Session.js';

const options: TypeNexusOptions = {
  // ...
  dataSourceOptions: { ... },
  session: {
    secret: 'secret',
    resave: false,
    saveUninitialized: false,
    repositoryTarget: Session,
    typeormStore: {
      cleanupLimit: 2,
      // limitSubquery: false, // If using MariaDB.
      ttl: 86400,
    }
  }
}

;(async () => {
  const app = new TypeNexus(3001, options);
  // ❶ Performs connection to the database.
  await app.connect();
  // OR: 
  // await app.connect(options.dataSourceOptions);

  // ❷ 🚨 Please be sure to use it after `app.connect()`.
  app.controllers([UserController]);
  // ❸ Listen for connections.
  await app.start();

})();

Here is the database table entity for Session:

// ./entity/Session.js
import { Column, Entity, Index, PrimaryColumn, DeleteDateColumn } from 'typeorm';
import { ISession } from 'connect-typeorm';

@Entity()
export class Session implements ISession {
  @Index()
  @Column('bigint', { transformer: { from: Number, to: Number } })
  public expiredAt = Date.now();

  @PrimaryColumn('varchar', { length: 255 })
  public id = '';

  @DeleteDateColumn()
  public destroyedAt?: Date;

  @Column('text')
  public json = '';
}

Using authorization features

TypeNexus comes with two decorators helping you to organize authorization in your application.

@Authorized decorator

To make @Authorized decorator to work you need to setup special TypeNexus options:

const app = new TypeNexus(3002, { ... });
await app.connect();

app.authorizationChecker = async (action: Action, roles: string[]) => {
  // here you can use request/response objects from action
  // also if decorator defines roles it needs to access the action
  // you can use them to provide granular access check
  // checker must return either boolean (true or false)
  // either promise that resolves a boolean value
  // demo code:
  const token = action.request.query.token || action.request.body.token || (action.request.headers.authorization || '').replace(/^token\s/, '');
  if (action.request.session.token !== token) return false;
  const dataSource = action.dataSource;
  const user = await dataSource.manager.findOne(User, {
    where: { username },
    select: ['username', 'id', 'roles'],
  });
  if (user && roles.find(role => user.roles.indexOf(role) !== -1)) return true;
  // @ts-ignore
  if (action.request.session.token === token) return true;
  return false;
}

app.controllers([UserController]);
await app.start();

You can use @Authorized on controller actions:

import { Controller, Authorized, Req, Res, Get } from 'typeorm';
import { Response, Request }from 'express';

@Controller()
export class UserController {
  @Authorized('POST_MODERATOR') // you can specify roles or array of roles
  @Post('/posts') // => POST /posts
  create(@Body() post: Post, @Req() request: Request, @Res() response: Response) {
    // ...
  }
}

@CurrentUser decorator

To make @CurrentUser decorator to work you need to setup special TypeNexus options:

import { TypeNexus, Action } from 'typenexus';
import { UserController } from './UserController.js';
import { User } from './User.js';

;(async () => {
  const app = new TypeNexus(3002, {
    routePrefix: '/api',
    developmentMode: false,
  });

  app.currentUserChecker = async (action: Action) => {
    return new User(1, 'Johny', 'Cage');
  }

  app.controllers([UserController]);
  await app.start();
})();

You can use @CurrentUser on controller actions:

import { Controller, CurrentUser, Get } from 'typenexus';
import { User } from './User.js';

@Controller('/questions')
export class UserController {
  @Get()
  public async all(@CurrentUser() user?: User): Promise<any> {
    return {
      id: 1,
      title: 'Question by ' + user.firstName,
    };
  }
}

If you mark @CurrentUser as required and currentUserChecker logic will return empty result, then TypeNexus will throw authorization required error.

Using middlewares

Global middlewares

Global middlewares run before each request, always. To make your middleware global mark it with @Middleware decorator and specify if it runs after or before controllers actions.

import { ExpressMiddlewareInterface } from 'typenexus';
import { Request, Response, NextFunction } from 'express';

@Middleware({ type: 'before' })
export class LoggingMiddleware implements ExpressMiddlewareInterface {
  use(request: Request, response: Response, next: NextFunction): void {
    console.log('do something...');
    // @ts-ignore
    request.test = 'wcj';
    next();
  }
}

To enable this middleware, specify it during typenexus initialization:

import { TypeNexus } from 'typenexus';
import './LoggingMiddleware.js';

const app = new TypeNexus(3002, {
  routePrefix: '/api',
  developmentMode: false,
});

Or register with app.controllers().

import { TypeNexus } from 'typenexus';
import { LoggingMiddleware } from './LoggingMiddleware.js';
import { UserController } from './UserController.js';

const app = new TypeNexus(3002, {
  routePrefix: '/api',
  developmentMode: false,
});
app.controllers([UserController], [LoggingMiddleware]);

Contributors

As always, thanks to our amazing contributors!

Made with contributors.

License

This package is licensed under the MIT License.