pointyapi
    TypeScript icon, indicating that this package has built-in type declarations

    3.4.0 • Public • Published

    PointyAPI

    "Stop writing endpoints"

    Typescript RESTful Server Architecture

    Build Status Coverage Status.

    Created and maintained by Stateless Studio

    Introduction

    PointyAPI is a library for quickly creating robust API servers.

    • ORM (TypeORM) Create models which automatically create and maintain your database
    • Validation (Class Validator) Use Typescript decorators to automatically validate fields
    • Authentication (JWT) JWT and request.user make authorization a breeze
    • Authorization Use Guards, CanRead(), and CanWrite() fields to ensure the user can read/write specific fields

    Models

    Models are created as Typescript classes. Here's an example class for a basic user:

    @Entity()
    class User extends BaseUser
    {
    	// User ID
    	@PrimaryGeneratedColumn() // Primary column
    	@IsInt() // Validation - Value must be integer
    	@BodyguardKey() // Authentication - User must match this to be considered "self"
    	@AnyoneCanRead() // Authorization - Anyone is allowed to read this field
    	public id: number = undefined;
    
    	// Username
    	@Column({ unique: true }) // Database column - Usernames are unique
    	@IsAlphanumeric() // Validation - must be alphanumeric
    	@Length(4, 16) // Validation - must be between 4-16 characters
    	@AnyoneCanRead() // Authentication - Anyone has read privelege to this member
    	@OnlySelfCanWrite() // Authorization - Only self can write this member
    	public username: string = undefined;
    
    	// Password
    	@Column() // Database column
    	@OnlySelfCanWrite() // Authentication - Only self can write this member
    	// No read key - nobody can read this field
    	public password: string = undefined;
    
    	/**
    	 * hashPassword
    	 */
    	public hashPassword() {
    		if (this.password) {
    			this.password = hashSync(this.password, 12);
    		}
    	}
    
    	/**
    	 * Hash the user's password before post
    	 */
    	public async beforePost(request: Request, response: Response) {
    		this.hashPassword();
    
    		return true;
    	}
    
    	/**
    	 * Hash the user's password on update
    	 */
    	public async beforePatch(request: Request, response: Response) {
    		this.hashPassword();
    
    		return true;
    	}
    }

    You can check out more sample models in examples/, or read more about them in the Models documentation.

    Routes

    Routes are Express routes which chain together PointyAPI middleware functions. Here's an example User router:

    const router: Router = Router();
    
    /**
     * Load model into PointyAPI
     */
    async function loader(request, response, next) {
    	if (await setModel(request, response, User)) {
    		next();
    	}
    }
    
    // POST - Load model, filter members, and save
    router.post('/', loader, postFilter, postEndpoint);
    
    // GET - Load model, filter members, and send
    router.get('/', loader, getFilter, getEndpoint);
    
    // PATCH - Load model, check if user owns, filter members, and save
    router.patch(`/:id`, loader, onlySelf, patchFilter, patchEndpoint);
    
    // DELETE - Load model, check if user owns, and delete
    router.delete(`/:id`, loader, onlySelf, deleteEndpoint);
    
    export const userRouter: Router = router;

    Getting Started

    Prerequisites

    • NodeJS
    • Database (Postgres Recommended)
    • HTTP Testing Client (Postman, cUrl, etc)
    • TS-Node
    • npm i -g ts-node

    Install

    Create a folder for your project, and run these commands:

    cd my-project
    npm init -y
    npm i pointyapi
    

    Create project

    1. Create a source directory, src

    2. Inside src/, create a file index.ts

      // src/index.ts
      
      import { pointy } from 'pointyapi';
      import { basicCors, loadUser } from 'pointyapi/middleware';
      const ROOT_PATH = require('app-root-path').toString();
      
      // Routes
      // TODO: We will import routes here
      
      // Setup
      pointy.before = async (app) => {
      	// CORS
      	app.use(basicCors);
      
      	// Load user
      	app.use(loadUser);
      
      	// Routes
      	// TODO: We will set our routes here
      
      	// Database
      	await pointy.db
      		.setEntities(
      			[
      				/* TODO: We will set our models here */
      				ExampleUser
      			]
      		)
      		.connect(ROOT_PATH)
      		.catch((error) => pointy.error(error));
      };
      
      // Start the server!
      pointy.start();
    3. Create a user route

      By default, PointyAPI will use ExampleUser as the user model. Let's create a route for this model, so that we can access this model through our API:

      • Create a folder for routes, src/routes/
      • Create a new router file, src/routes/user.ts
      • Copy & paste router code:
      // src/routes/user.ts
      
      import { Router } from 'express';
      import { setModel } from 'pointyapi';
      import { ExampleUser } from 'pointyapi/models';
      import { postFilter, getFilter, patchFilter } from 'pointyapi/filters';
      import { onlySelf } from 'pointyapi/guards';
      import {
      	getEndpoint,
      	postEndpoint,
      	patchEndpoint,
      	deleteEndpoint
      } from 'pointyapi/endpoints';
      
      const router: Router = Router();
      
      // Set the route model to ExampleUser
      async function loader(request, response, next) {
      	if (await setModel(request, response, ExampleUser)) {
      		next();
      	}
      }
      
      // Route endpoints
      router.get('/', loader, getFilter, getEndpoint);
      router.post('/', loader, postFilter, postEndpoint);
      router.patch(`/:id`, loader, onlySelf, patchFilter, patchEndpoint);
      router.delete(`/:id`, loader, onlySelf, deleteEndpoint);
      
      export const userRouter: Router = router;
    4. Import user route into server

      Open src/index.ts up again, and let's import our new User route.

      import { ExampleUser } from 'pointyapi/models'; // Add import to our user model
      
      ...
      // Routes
      // TODO: We will import routes here
      import { userRouter } from './routes/user'; // Add import to our user route
      
      ...
      
      // Routes
      // TODO: We will set our routes here
      app.use('/api/v1/user', userRouter); // Add our user route to the app
      
      // Database
      await pointy.db
      	.setEntities(
      		[
      			/* TODO: We will set our models here */
      			ExampleUser // Add our BaseModel model to the database
      		]
      	)
      
      ...
    5. Setup package.json

      Add a start script to package.json:

      "scripts": {
      	"start": "ts-node src/index.ts"
      }
    6. Setup database

      Create a database, create a local.config.json file in the root folder of your app, and replace the values:

      {
      	"type": "postgres",
      	"host": "localhost",
      	"port": 5432,
      	"user": "MY_DATABASE_USERNAME",
      	"password": "MY_DATABASE_PASSWORD",
      	"database": "MY_DATABASE_NAME"
      }
    7. Start & Test

      Our server is ready to run:

      npm start
      

      Open Postman, and send a GET request for all users. You'll see the result as an empty array, as there are no users yet: postman

      Create a user:

      We'll send a POST request to /api/v1/user, with the JSON body of our new user postman

      Let's get all users again: postman

      You can see that now a get request for all users produces an array of our one user.

    8. Authentication

      So we can get and post users, but what if we try to delete or update a user? Let's try it: postman

      So the server responded with 401 Unauthorized, and a body of "not self". What gives?

      Open at our user router (/src/routes/user.ts). Look at our DELETE route:

      router.delete(`/:id`, loader, onlySelf, deleteEndpoint);

      Notice onlySelf - that means only the user can access this route. We're not logged in yet, so we're certainly not "self"

      Create another router file, src/routes/auth.ts. This will be our authentication route so that we can log-in.

      // src/routes/auth.ts
      
      import { Router } from 'express';
      import { loginEndpoint, logoutEndpoint } from 'pointyapi/endpoints';
      import { ExampleUser } from 'pointyapi/models';
      import { setModel } from 'pointyapi';
      
      const router: Router = Router();
      
      // Set our route model & activate auth route
      async function loader(request, response, next) {
      	if (await setModel(request, response, ExampleUser, true)) {
      		next();
      	}
      }
      
      // Router endpoints
      router.post('/', loader, loginEndpoint);
      router.delete('/', loader, logoutEndpoint);
      
      export const authRouter: Router = router;

      And import this into our index.ts:

      ...
      // Routes
      // TODO: We will import routes here
      import { userRouter } from './routes/user';
      import { authRouter } from './routes/auth'; // Add import to our auth route
      
      ...
      
      // Routes
      // TODO: We will set our routes here
      app.use('/api/v1/user', userRouter);
      app.use('/api/v1/auth', authRouter); // Add our user route to the app
      
      // Database
      await pointy.db
      	.setEntities(
      		[
      			/* TODO: We will set our models here */
      			ExampleUser
      		]
      	)
      
      ...

      Restart the server (ctrl+c to stop the server)

      We can login with Postman: postman

      We received a "token" back (yours will be different):

      "token": "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBaQ0k2TVN3aWFXRjBJam94TlRVeE1qa3dOREkxTENKbGVIQWlPakUxTlRFek1EUTRNalY5Lk40UWNJV1hPYWVzWW9KOWh4VGx1X182dnhNVndpbkFXNGhRY2JWNkRKSUE="

      We can now use that token to delete our user: postman

      Notice that now we get a 204 No Content (which means deleted successfully!).

      What about the refreshToken? You may notice you got two tokens back: token and refreshToken.

      The token is short-lived - it only lasts 15 minutes or so. The refreshToken is long-living - it lasts about a week.

      You can use the refreshToken to issue a new accessToken when it expires.

    9. Production

    To launch in production mode, please make sure the following variables are set (environment variables/.env)

    • SITE_TITLE - Set the site title
    • ALLOW_ORIGIN - Set your client URL to add the client to the CORS policy
    • JWT_KEY - Set your token key to make JWT cryptographically secure
    • JWT_TTL - Set your token time-to-live (seconds). Default is 15 minutes
    • JWT_REFRESH_TTL - Set your refresh token time-to-live (seconds). Default is 7 days.

    UUID vs Auto-Incremented IDs

    It is a security risk to use auto-incremented IDs, and you should instead use UUID for all ID columns.

    To switch to using UUIDs:

    • Install the pgcrypto extension
    • Tell TypeORM to use pgcrypto by placing the line uuidExtension: 'pgcrypto' in orm-cli.js
    • Change all PrimaryGeneratedColumn() to PrimaryGeneratedColumn('uuid')
    • Change all public id: number members to public id: string
    • Make sure you remove IsInt() from all ID fields, if it exists
    • Ensure your front-end and anywhere you may access the ID is expecting a string

    Example:

    	// User ID
    	@PrimaryGeneratedColumn('uuid') // Primary column
    	@BodyguardKey() // Authentication - User must match this to be considered "self"
    	@AnyoneCanRead() // Authorization - Anyone is allowed to read this field
    	public id: string = undefined;

    Continued Reading

    PointyAPI

    Read more about PointyAPI by cloning the repository and building the docs:

    git clone https://github.com/StatelessStudio/pointyapi
    cd pointyapi
    npm install --ignore-scripts
    npm i -g typedoc
    npm run docs
    

    Now open docs/index.html in your web browser.

    You can also check out the examples in test/examples and even check out the test specs in test/specs

    TypeORM

    Read more about TypeORM: https://github.com/typeorm/typeorm

    Class Validator

    Read more about Class Validator: https://github.com/typestack/class-validator

    Express

    Read more about Express: https://www.express.com/

    Securing your Server

    Read the security checklist: https://github.com/shieldfy/API-Security-Checklist

    Install

    npm i pointyapi

    DownloadsWeekly Downloads

    25

    Version

    3.4.0

    License

    MIT

    Unpacked Size

    411 kB

    Total Files

    277

    Last publish

    Collaborators

    • stateless-studio