npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

pointyapi

v4.0.0

Published

*"Stop writing endpoints"*

Downloads

115

Readme

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, bootstrap, log } from 'pointyapi';
    import { basicCors, loadUser } from 'pointyapi/middleware';
    
    // 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()
    		.catch((error) => pointy.error(error));
    };
    
    // Start the server!
    bootstrap(async () => await 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 .env file in the root folder of your app, and replace the values:

    POINTYAPI_DB_NAME=pointyapi
    POINTYAPI_DB_TYPE=postgres
    POINTYAPI_DB_HOST=localhost
    POINTYAPI_DB_PORT=5432
    POINTYAPI_DB_USER=pointyapi
    POINTYAPI_DB_PASS=password1234
  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;

Logger

PointyAPI uses ts-tiny-logger.

Using the Log

import { log } from 'pointyapi';

log.fatal('Oh no!', { someData: 'foo' });
log.error('Error!', new Error());
log.warn('Warning!', 1234);
log.info('Information!', 'Hello!');
log.debug('Debugging...', { someData: 'bar' });

Setting the Log

import { setLog } from 'pointyapi/log';
import { Log } from 'ts-tiny-log';
import { LogLevel } from 'ts-tiny-log/levels';

setLog(new Log({
	level: LogLevel.info,
}));

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