instant-express-core
v0.0.2
Published
Core [`instant-express`](https://github.com/cjmyles/instant-express) functionality, including routing, authentication, session, firebase connectivity and utilities.
Downloads
3
Maintainers
Readme
Instant Express Core
Core instant-express
functionality, including routing, authentication, session, firebase connectivity and utilities.
Please note: this module contains some code that will assist with the creation of a Firebase Functions application. However, you can easily use this module for any Express app, and other database/hosting drivers will be added in the future.
Features
- Basic Authentication
- CORS Middleware
- Winston Logging
- Automatic Routing
- Session
- Firebase Admin initialisation
- Generic Actions and Controller classes that can be easily extented to enable simple CRUD routing operations
- Firebase Firestore specific Repository class to enable retrieval of Firestore collections and documents
Sample Usage
const express = require('express');
const InstantAPI = require('instant-express-api');
const app = express();
// const config = ...
const instant = InstantAPI.initialize(config);
// Create our app
const app = express();
// Initialise InstantAPI
const instant = InstantAPI.initialize(config);
// Set up CORS
app.use(instant.cors());
// Set up auth
app.use(instant.auth());
// Enable logging
app.logger = instant.logging();
// or, `logger = instant.logging();` if you want a global variable
// Set up firebase
app.firebase = instant.firebase();
// or, `firebase = instant.firebase();` if you want a global variable
// Set up the routes
app.use(instant.routes());
Configuration
Some features require a configuration object. Options should be passed in on initialisation as a json object. I recommend using something like config to access your configuration settings, but you could equally implement something simpler.
Example
"auth": {
"method": "basic",
"users": {
"craig": "pa55w0rd",
"admin": "supersecret",
}
},
"logging": {
"error": {
"filename": "error.log"
},
"combined": {
"filename": "combined.log"
}
},
"routes": {
"path": "api/routes"
},
Authentication
Basic
Utilises Express Basic Auth.
Sample Usage
app.use(
instant.auth({
method: 'basic',
users: {
craig: 'pa55w0rd',
admin: 'supersecret',
},
})
);
Options
| Name | Type | Description | Options |
| :------- | :------- | :-------------------------------- | :------ |
| method
| string
| Authentication type | "basic" |
| users
| object
| username:password key value pairs |
CORS
Utilises CORS.
Sample usage
app.use(instant.cors());
Options
N/A
Logging
Utilises Winston.
Sample usage
app.logger = instant.logging({
error: {
filename: 'error.log',
},
combined: {
filename: 'combined.log',
},
});
Options
| Name | Type | Description | Options |
| :------------------ | :------- | :--------------------------- | :------ |
| error.filename
| string
| File to log errors to |
| combined.filename
| string
| File to log combined logs to |
Routes
Autoconfigure API routes (rather than manually importing each routes file one by one). Can be configured to utilise routes files from one directory (e.g. ./api/routes/
), or within base directories (useful for versioned routes such as /api/v1.0/routes
).
For example, we can expose a GET method on /api/test/
assuming we've have ./api/routes/test.js
that looks something like this:
// ./api/routes/test.js
import express from 'express';
const router = express.Router();
router.get('/', (req, res) => {
res.send({ testing: 123 });
});
module.exports = router;
Sample usage
app.use(
instant.routes({
path: 'api/routes',
})
);
Options
| Name | Type | Description | Options |
| :---------------- | :------- | :--------------------------------------------- | :------ |
| base
(optional) | string
| Location of base API directory, e.g. api
|
| path
| string
| Location of API routes directory, e.g routes
|
| prefix
| string
| Prefix to apply to routes, e.g. api
|
Note: you can have multiple named directories that sit between the base and path, all of which will be automatically parsed, for example ./api/v1.0/routes/test.js
and ./api/v2.0/routes/test.js
.
Session
Utilises Express Session or Cookie Session.
Sample usage
app.use(instant.session({
"type": "express",
"options": {
"secret": "super-secret-phrase",
"resave": false,
"saveUninitialized": false,
"cookie": {}
}
});
Options
| Name | Type | Description | Options |
| :-------- | :------- | :------------------------------------------------------------------ | :------------------ |
| type
| string
| Session type | express
, cookie
|
| options
| object
| Module specific options (see specific package for more information) |
Firebase
Initialises our Firebase app and exposes the following services:
- Auth (coming soon)
- Firestore
- Storage (coming soon)
Sample usage
app.use(instant.firebase({
'serviceAccountKey': 'config/serviceAccountKey.json'
});
Options
| Name | Type | Description | Options |
| :------------------ | :------- | :-------------------------------------------------------------------------------------------------- | :------ |
| serviceAccountKey
| string
| Location of Service Account Key JSON file (please see https://firebase.google.com/docs/admin/setup) |
Classes
Writing entity specific routes can be time consuming, especially when you have a lot of routing code replicated across multiple entities performing the same task. For example, imagine you have two API routes, /api/clients
and /api/users
that both have create, find all, find one, find by id, update and delete operations. The majority of the code will be same across both routes, performing functions such as fetching from the database, testing for the existence of a returned record or document, and handling the response.
To alleviate this issue, this module includes classes that can perfom these basic tasks, which you can extend from for your own needs. However, this would assume the following code structure:
/api
/routes
/test.js
/actions
/test.js
/controllers
/test.js
/repositories
/test.js
- A
routes
method defines whichactions
method to execute for a given request. - An
actions
method defines whichcontroller
method to execute. - A
controller
method defines whichrepository
method to execute. - A
repository
method communicates with a database and returns the results.
Data is passed back from the repository to the action which handles the response. This might seem over elaborate, but provides a clear separation of concerns between the tasks being performed. It also provides the following benefits:
- It would be relatively simple to swap one
repository
out for another. For example, as long as the same methods existed, we could swap between using a Mongo db and Firestore. - Although an
actions
could communicate directly withrespositories
,controllers
enable us to perform calculations or operations on the results before they are returned. This means thatcontrollers
could reference othercontrollers
and the calculations or operations happen in one place. It also separates what might be send in a request from the required inputs for retrieving or calculating data.
Note: although not specifically referenced here, I would suggest also having an adapters
directory for handling operations to other 3rd party APIs (such as Twilio or Sendgrid), as well as a helpers
directory for common entity specific utilities such as validators or formatters.
The following operations are currently supported:
create
createMany
find
findOne
findById
update
updateOrCreate
delete
Actions
| Method | Expected request | Type | Description | Example |
| :--------------- | :------------------------- | :------- | :---------------------------------------- | :----------------------- |
| create
| req.body
| object
| Attributes to create document with | { "name": "Craig" } |
| | req.params.id
(optional) | string
| Id to create document with | "00001" |
| find
| req.query
| string
| Search query | "name=Craig" |
| findOne
| req.query
| string
| Search query | "name=Craig" |
| findById
| req.params.id
| string
| Document id | "00001" |
| update
| req.body
| object
| Attributes to update document with | { "location": "Sydney" } |
| updateOrCreate
| req.query
| string
| Search query | "name=Craig" |
| | req.body
| object
| Attributes to update/create document with | { "location": "Sydney" } |
| delete
| req.params.id
| string
| Document id | "00001" |
Sample usage
// ./api/routes/clients.js
const express = require('express');
const actions = require('../actions/clients');
const router = express.Router();
router.post('/', actions.create);
router.post('/:id', actions.create);
router.get('/', actions.find);
router.get('/:id', actions.findById);
router.put('/:id', actions.update);
router.delete('/:id', actions.delete);
module.exports = router;
// ./api/actions/clients.js
const Actions = require('instant-express-api').Actions;
const controller = require('../controllers/clients');
// const HttpStatusCodes = require('http-status-codes');
class Clients extends Actions {
// Includes the following methods by default:
// create, createWithId, createMany, find, findOne, findById, update, updateOrCreate and delete
//
// e.g.
// async create(req, res, next) {
// try {
// let response = req.params.id
// ? await this.controller.createWithId(req.params.id, req.body)
// : await this.controller.create(req.body);
// res.status(HttpStatusCodes.OK).send(response);
// } catch (error) {
// res.status(HttpStatusCodes.OK).send(error);
// }
// }
//
// Custom methods here...
// Note: we have to bind the method to `this`
//
// e.g.
// constructor(controller) {
// this.custom = this.custom.bind(this);
// super(controller);
// }
//
// async custom(req, res, next) {
// try {
// let response = await this.controller.custom(req.params.id)
// res.status(HttpStatusCodes.OK).send(response);
// } catch (error) {
// res.status(HttpStatusCodes.OK).send(error);
// }
// }
}
module.exports = new Clients(controller);
// api/controllers/clients.js
const Controller = require('instant-express-api').Controller;
const repository = require('../repositories/clients');
class Clients extends Controller {
// Includes the following methods by default:
// create, createWithId, createMany, find, findOne, findById, update, updateOrCreate and delete
//
// e.g.
// async create(attributes) {
// try {
// return await this.repository.create(attributes);
// } catch (error) {
// throw error;
// }
// }
//
// Custom methods here...
// Note: we have to bind the method to `this`
//
// e.g.
// constructor(repository) {
// this.custom = this.custom.bind(this);
// super(repository);
// }
//
// async custom(id) {
// try {
// return await this.repository.custom(id)
// } catch (error) {
// res.status(HttpStatusCodes.OK).send(error);
// }
// }
}
module.exports = new Clients(repository);
// api/repositories/clients.js
const FirestoreRepository = require('instant-express-api').FirestoreRepository;
const firebase = require('../../firebase'); // Your firestore initialisation script (see below)
class Clients extends FirestoreRepository {
// Includes the following methods by default:
// create, createWithId, createMany, find, findOne, findById, update, updateOrCreate and delete
//
// e.g.
// async create(attributes) {
// try {
// const ref = await this.db.collection(this.collection).add(attributes);
// if (ref.id) {
// return await this.findById(ref.id);
// }
// } catch (error) {
// throw error;
// }
// }
//
// Custom methods here...
// Note: we have to bind the method to `this`
//
// e.g.
// constructor(db, collection) {
// this.custom = this.custom.bind(this);
// super(db, collection);
// }
//
// async custom(id) {
// try {
// return await this.db.collection(this.collection).get(id);
// } catch (error) {
// res.status(HttpStatusCodes.OK).send(error);
// }
// }
}
module.exports = new Clients(firebase.db, 'clients');
// firebase.js - see https://firebase.google.com/docs/admin/setup
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
const db = admin.firestore();
const settings = { /* your settings... */ timestampsInSnapshots: true };
db.settings(settings);
module.exports = { db };
Running Tests
Please note: tests haven't yet been implemented (https://auth0.com/blog/developing-npm-packages/).
To run the tests, clone the repository and install the dependencies:
git clone https://github.com/JSJInvestments/instant-express-api.git
cd instant-express-api && npm i
npm run test