Use Functional Programming to Simplify Your Meteor Code

Speaking of functional programming… i’m wondering if anyone would be interested in me open sourcing a little library I wrote for a RESTful API ? I’m not sure how useful this would be.

Personally with a bit more polish I think having this on a headless Meteor server is more productive than an express API (no callbacks!)

I started out with just the Picker package and ran into maintenance problems so I made this.

@awatson1978 i’d love your opinion if you have a few spare mins!

It’s written in a functional style and heavily utilizes ES6 to create a better developer API. It was evolved from the simple:json-routes package and into something that can be used to easily maintain a large API using Meteor as the base. I’m currently using it to power a set of native and ReactNative apps.

The main features are:

  • Functional approach with ES6
  • Automatically catch errors and render proper JSON error
  • Easy to use router
  • Skinny controller to ease testing and to isolate side effects
  • Connection transformation in controller with chaining functions (like change header data)
  • Changeset integration with SimpleSchema
  • Optional JSON view layer for response data munging
  • CLI generator (with passing tests) if it would get open-sourced

Also some non-router things that I used are (and perhaps a sep package):

  • Repository pattern to abstract away database & throw errors if $ is prepended, eg. $insert
  • Model layer (works with changeset to validate insert/updates)

Here’s the gist of how it works:

Router

const {scope, get, post, put, delete_} = Restful;

applyMiddleware(
  acceptJSON(),
  authenticateJWT({except: 'post.user'}),
);

scope('/v1', () => {
  // resource saves boilerplate using conventions
  resources('/users', UserController, {except: 'delete'})
  resources('/comments', CommentController)
  resources('/posts', PostController)

  // or manually add routes
  get('/items/', ItemController, 'index')
  post('/items', ItemController, 'create')
  get('/items/:id', ItemController, 'show')
  delete_('/items/:id', ItemController, 'delete')
  put('/items/:id', ItemController, 'update')
})

Controller

const {sendJson, putStatus} = Restful;
const {Item} from Models;
const {ItemView} from Views;

ItemController = {
  debug: true, // prints params and return value
  scrubParams: [...], // pending, will reject all contrl reqs based on rules

  index(conn, params) {
    // Repository pattern abstracts persistence dep. and removes boilerplate
    const items = Repo.all(Item);
    // allows a json 'view' to munge data outside of controller (optional)
    return renderView(conn, ItemView, items);
    //or just directly send:  `sendJson(conn, items)`
  },

  create(conn, params) {
    // you could use Repo.$insert to remove error handling
    // boilerplate, see #show & #delete actions
    const [status, result] = Repo.insert(params);
    if (status === 'ok') {
      return sendJson(conn, result);
    } else {
      return sendJson(conn, 500, {error: result})
    }
  },

  update(conn, params) {
    const [status, result] = Repo.update(Item, params.id, params);
    if (status === 'ok') {
      return sendJson(conn, result);
    } else {
      return sendJson(conn, 500, {error: result});
    }
  },

  show(conn, {id}) {
    // $get will throw and return json error if non-existent
    // or get will return undefined if non-existent
    const item = Repo.$get(Item, id);
    return sendJson(conn, item);
  },

  delete(conn, {id}) {
    Repo.$delete(Item, id);
    return sendJson(conn, {});
  }
};

Repo layer (Optional)

Too much for this post, see gist:
https://gist.github.com/AdamBrodzinski/9e667d14e980df7c0fbb

JSON View layer (Optional)

const {renderMany, renderOne} = Restful;

// you can munge data as needed without concerning the database
// this makes testing the transformation much easier
// ex in controller:
//
//     return renderView(conn, ReminderView, reminders);
//
ReminderView = {
  // can use conn to determine auth/user/params/etc..
  // munge ex: add 'data: {}' wrapper, convert time to unix timestamp, etc..
  render(conn, reminder) {
    return {
      _id: reminder._id,
      scheduledAt: reminder.scheduledAt,
      userId: reminder.userId,
      userName: reminder.userName,
      userNumber: reminder.userNumber,
      message: reminder.message,
      recurFrequency: reminder.recurFrequency,
      active: reminder.active,
      recipients: reminder.recipients,
      sentLog: reminder.sentLog
    };
  }
}
3 Likes