0.0.17 • Published 3 months ago

apinion v0.0.17

Weekly downloads
5
License
MIT
Repository
github
Last release
3 months ago

APInion

an opinionated API framework built on express

  • Presently this framework is not ready for production use outside of my own projects, proceed at your own risk.

Quick Start

The fastest way to get started from scratch:

mkdir my-api
cd my-api
npm init -y
npm install --save apinion

Then create a file called index.mjs with the following contents (or any contents from the examples above):

import { Router } from 'apinion';

const router = new Router();

router.enableCors();

router.get('/', { name: 'root' }, () => {
  return {
    hello: 'world',
  };
});

router.listen(9166);

Then run node index.mjs and you should be able to hit http://localhost:9166 and see the response.

Consider modifying your package json's scripts to include the start script:

"scripts": {
  "start": "node --experimental-modules index.mjs"
},

API Documentation

import { Router } from 'apinion';

const router = new Router();

router.enableCors();

router.get('/endpoint', { name: 'name' }, () => {
  return {
    data: 'this will be returned as json to the end user'
  };
});

router.listen(9512);

helmet recommended

import helmet from 'helmet';
import { Router } from 'apinion';

const router = new Router();
router.use(helmet());

router.listen(9494);

using middleware like multer

import multer from 'multer';
import { Router } from 'apinion';

const router = new Router();

router.get('/upload', { middleware: multer({ dest: '/tmp/' }).single('File') }, ({ request }) => {
  // do whatever you want with request.file, request.file.path contains the temporary file path
});

router.listen(5934);

using router arrays

import { Router, makeEndpoint } from 'apinion';

const router = new Router();
const endpoint = {
  config: { required: ['secret'] },
  callback: ({ required }) => {
    return 'your secret is ' + required.secret;
  }
};

const anotherEndpoint = makeEndpoint({ name: 'test' }, () => {
  return [1, 2, 3];
});

const routeArray = [
  { path: 'v1', subrouter: [
    { path: '/some_secret', get: endpoint },
    { path: '/inline', any: { config: { name: 'hi' }, callback: () => 'inline created' } },
  ]},
  { path: '/test', get: anotherEndpoint },
];

router.applyRoutes(routeArray);

now you can hit yourapi/v1/some_secret?secret=hi and yourapi/test

promises

  • promises are accepted as endpoint callbacks
import { Router } from 'apinion';

const router = new Router();

router.post('/async', {}, () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve('this took a second'), 1000);
  });
});

router.listen(4495);

authentication

  • You can require authentication functions on a subrouter or on individual endpoints

subrouter authentication:

import { Router, HttpError } from 'apinion';

const router = new Router();
const adminSubrouter = router.subrouter('/admin');
const users = {
  jerry: { username: 'jerry', password: 'friar', admin: true },
  bob: { username: 'bob', password: 'friar', admin: false },
};

adminSubrouter.setAuthenticator(({ request, body, query, headers }) => {
  const usable = body || query;
  const user = users[usable?.username];
  if (!user || user?.password !== usable?.password) {
    throw new HttpError({ status: 401, message: 'Bad creds' });
  }
  if (!user?.admin) {
    throw new HttpError({ status: 403, message: 'Not allowed' });
  }
  return user;
});

adminSubrouter.get('/hi', { name: 'super secret admin thing', secret: true }, ({ identity, body }) => {
  return { identity, body };
});

router.listen(10583);

endpoint authentication:

import { Router, makeHardcodedBasicAuthenticator } from 'apinion';

const router = new Router();
const tempAuthenticator = makeHardcodedBasicAuthenticator([{ username: 'joe', password: 'doe' }]);

router.get('/auth', { authenticator: tempAuthenticator }, ({ identity }) => {
  return identity;
});

router.listen(5550);

streaming post body

  • use noParse to prevent the input from being automatically parsed, in this mode the body parameter is guaranteed to be undefined
import { Router, makeHardcodedBasicAuthenticator } from 'apinion';
import fs from 'fs';

const router = new Router();

router.get('/streamable', { noParse: true }, ({ request }) => {
  const destination = fs.createWriteStream('filename.ext');
  request.pipe(destination);
  return new Promise(resolve => {
    destination.on('finish', () => {
      resolve({ message: 'wrote file', filename: 'filename.ext' });
    });
  });
});

router.listen(5550);

combined parameters and custom request auth:

import { Router, makeRequestAuthenticator } from 'apinion';

const router = new Router();
const tempAuthenticator = makeRequestAuthenticator((input) => {
  if (input?.headers?.secret === 'fancypants') return { admin: true };

  return null;
});

router.get('/paramtest', { authenticator: tempAuthenticator, required: ['a', 'b'], optional: ['c'] }, ({ identity, params }) => {
  if (identity?.admin) {
    return params.a + params.b + params.c;
  } else {
    return params.a;
  }
});

router.listen(5550);

makeEndpoint:

import { Router, makeEndpoint } from 'apinion';

const router = new Router();

const customEndpoint = makeEndpoint({ name: 'custom', required: ['z'] }, async (params) => {
  return { something: 'another' };
});

const customEndpoint2 = makeEndpoint({ name: 'custom2' }, async (params) => {
  return { message: 'hola' };
});

const customEndpoint3 = makeEndpoint({ name: 'custom3' }, async (params) => {
  return { action: 'do the thing' };
});

const routes = [
  {
    path: 'v1',
    subrouter: [
      { path: 'test', get: customEndpoint },
      { path: 'test2', post: customEndpoint2 },
      { path: 'test3', any: customEndpoint3 },
    ],
  }
];

router.applyRoutes(routes);

router.listen(5550);

// now you can request http://yourapi.com/v1/test?z=test

custom error handling

import { Router, makeEndpoint } from 'apinion';

const router = new Router();
router.addErrorHandler(({ error, config, request, response }) => {
  console.error('error handling', request.originalUrl, error);

  if (error?.status) {
    response.status(error.status).send({ message: error.message || 'unknown error' });
  } else {
    // error appears to not be an apinion HttpError
    response.status(500).send({ message: 'this is a custom error' });
  }

  // if you want to bubble to parent router
  // router.parent.onError({ error, config, request, response });
});

router.listen(5550);

logging

import { Router, makeEndpoint } from 'apinion';

const router = new Router();

// request start
router.use((req, res, next) => {
  console.log(new Date().toISOString(), req.method, req.url, 'from', req.headers['x-forwarded-for'] || req.connection.remoteAddress);
  next();
});

// request end
router.addResponseCallback(({ request, response, status }) => {
  console.log(new Date().toISOString(), req.method, req.url, 'from', req.headers['x-forwarded-for'] || req.connection.remoteAddress, status);
});

// request early termination (will not be seen in request end)
router.addEarlyDisconnectCallback(({ request, response, status }) => {
  console.log(new Date().toISOString(), 'EARLY TERMINATION', req.method, req.url, 'from', req.headers['x-forwarded-for'] || req.connection.remoteAddress, status);
});


router.listen(5550);

Troubleshooting

I'm getting an error about experimental modules

You need to run node with the --experimental-modules flag, or add "type": "module" to your package.json.

node --experimental-modules index.mjs
0.0.14

3 months ago

0.0.15

3 months ago

0.0.16

3 months ago

0.0.17

3 months ago

0.0.12

6 months ago

0.0.13

5 months ago

0.0.10

10 months ago

0.0.11

10 months ago

0.0.9

10 months ago

0.0.8

1 year ago

0.0.7

1 year ago

0.0.6

3 years ago

0.0.5

3 years ago

0.0.4

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago