Giter Site home page Giter Site logo

exprestive's Introduction

expRESTive

NPM Version Build Status Dependency Status

Add rails-style routes and controllers to Express.js (and other connect-based web frameworks).

Basic usage

Assuming you have already created an Express application by following the Express installation instructions. Now:

  • add expRESTive to your package.json file: $ npm install --save exprestive

  • add the expRESTive middleware to your application

    const express = require('express');
    const exprestive = require('exprestive');
    
    app = express();
    app.use(exprestive());
    
    app.listen(3000);
  • create a routes.js file in the same directory as your server

    // routes.js
    module.exports = ({ GET, POST, PUT, DELETE }) => {
      GET('/hello', { to: 'helloWorld#index' });
    };
  • create a controllers/ directory in the same directory as your server and populate it with controllers. Controller file names must end in controller (with a js or compile-to-js extension). This restriction can be changed with the controllersPattern option. See options.

    // controllers/hello_world_controller.js
    module.exports = class HelloWorldController {
      index(req, res) {
        res.end('hello world');
      }
    };
  • visit localhost:3000/hello in your browser

Reverse routing

Exprestive exports url building functions to this.routes in controllers and res.locals.routes for each request.

Saving / accessing reverse routes

In your routes file you can pass an as parameter to non-restful routes to define a reverse route.

// routes.js
module.exports = ({ GET }) => {
  GET('/foo/bar', { to: 'foo#bar', as: 'foobar' });
};

In a controller you can access this path with this.routes.foobar()

// controllers/foo_controller.js
module.exports = class FooController {
  bar(req, res) {
    this.routes.foobar(); // returns {path: "/foo/bar", method: "GET"}
  }
};

In a view you can access this path with routes.foobar()

//- index.jade
a(href=routes.foobar()) Visit foobar

Parameters

If a route has parameters, the reverse route can take the parameters in order as arguments or as an object

// routes.js
module.exports = ({ GET }) => {
  GET('/users/:userId/posts/:id', { to: 'posts#show', as: 'userPost' });
};

// controllers/posts_controller.js
class PostsController {
  show(req, res) {
    this.routes.userPost(1, 2).path;               // returns "/users/1/posts/2"
    this.routes.userPost({userId: 1, id: 2}).path; // returns "/users/1/posts/2"
  }
};

Restful routing

The resources helper can be used to build all the standard RESTFUL routes

// routes.js
module.exports = ({resources}) => {
  resources('users');
};

is equivalent to

// routes.js
module.exports = ({ DELETE, GET, POST, PUT }) => {
  GET(    '/users',          { to: 'user#index',   as: 'users'       });
  GET(    '/users/new',      { to: 'user#new',     as: 'newUser'     });
  GET(    '/users/:id',      { to: 'user#show',    as: 'user'        });
  GET(    '/users/:id/edit', { to: 'user#edit',    as: 'editUser'    });
  PUT(    '/users/:id',      { to: 'user#update',  as: 'updateUser'  });
  POST(   '/users',          { to: 'user#create',  as: 'createUser'  });
  DELETE( '/users/:id',      { to: 'user#destroy', as: 'destroyUser' });
};

You can limit the restful routing with the options except: or only:

// routes.js
module.exports = ({resources}) => {
  resources('users', {only: ['index', 'new', 'create', 'destroy']});
  resources('posts', {except: ['index']});
};

Scoped routing

The scope helper can be used to create a prefixed set of routes:

// routes.js
module.exports = ({ GET, scope }) => {
  scope('/api', () => {
    GET('/users', {to: 'users#index'});
    GET('/widgets', {to: 'widgets#index'});
  });
};

This is equivalent to:

// routes.js
module.exports = ({ GET }) => {
  GET('/api/users', { to: 'users#index' });
  GET('/api/widgets', { to: 'widgets#index' });
};

Scopes can also be nested:

// routes.js
module.exports = ({ GET, resources, scope }) => {
  scope('/api', () => {
    scope('/v1', () => {
      resources('users');
      GET('/widgets', { to: 'widgets#index' });
    });
  });
};

Middleware Support

Adding per-route middleware can be done in the controller by setting middleware.

// controllers/hello_world_controller.js
const someMiddleware = require('some-middleware');

module.exports = class HelloWorldController {
  constructor() {
    this.middleware = { index: someMiddleware };
  }

  index(req, res) {
    res.end('hello world');
  }
};

The specified middleware will be inserted in the chain before the controller action. An array of middleware can also be specified and they will be inserted in the chain in the specified order.

BaseController

A base controller has been exposed that application controllers can optionally extend. The base controller exposes several helper methods.

The useMiddleware helper adds middleware declartions for all controller actions. An array of middleware can also be specified and they will be inserted in the chain in the specified order. Often this would be used in the constructor of a controller. The function also accepts options of only or except which modify the list of actions.

// controllers/hello_world_controller.js
const BaseController = require('exprestive').BaseController;
const someMiddleware = require('some-middleware');
const someOtherMiddleware = require('some-other-middleware');

module.exports = class HelloWorldController extends BaseController {
  constructor() {
    super();
    this.useMiddleware(someMiddleware);
    this.useMiddleware(someOtherMiddleware, {only: 'index'});
  }

  index(req, res) {
    res.end('hello world index');
  }

  show(req, res) {
    res.end('hello world show');
  }
};

The getActions helper returns an array of all the actions on the controller. It also takes options of only or except to modify the list.

Options

Options are provided to the exprestive function.

app = express();
app.use(exprestive({ appDir: './www' }));
  • appDir
    • Directory used as a base directory for routes file and controllers directory
    • default: __dirname of the file that calls exprestive()
  • controllersPattern
    • Glob pattern used to find controllers. Resolved relative to appDir
    • default: 'controllers/*controller.+([^.])'
  • dependencies
    • Object passed to controller constructors
    • default: {}
  • routesFilePath
    • Path to routes file. Resolved relative to appDir. This is passed to require, so extension is optional
    • default: 'routes'

Development

See the developer documentation

exprestive's People

Contributors

alexdavid avatar alexpeachey avatar charlierudolph avatar kevgo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

exprestive's Issues

Use simple *.js files.

Hi, I would just like to know if I could use *.js files instead of *.coffee files ? If yes, can you shed some light on how can i do that.

Better errors

Referencing a controller that doesn't exist in a routes file returns the error

TypeError: Cannot call method 'replace' of undefined

Better error messages all around would help debugging when using exprestive magic.

adding middleware on a per-scope basis

We'd like to be able to scope some middleware functions to authenticate for a group of routes. Right now to do that we would have to add middleware to each controller or break out of exprestive to run middleware for a given set of scoped routes.

I'd like to be able to do something like this:

module.exports = ({ GET, scopeWithMiddleware }) => {
  scopeWithMiddleware('/api', {to: 'auth#validate'},  () => {
    GET('/users', {to: 'users#index'});
    GET('/widgets', {to: 'widgets#index'});
  });
};

maybe there's a way to also handle cases were the middleware is just a function and not a class. Perhaps just requiring it and handing it off?

const validate = require('./middleware/validate')

module.exports = ({ GET, scopeWithMiddleware }) => {
  scopeWithMiddleware('/api', {to: validate},  () => {
    GET('/users', {to: 'users#index'});
    GET('/widgets', {to: 'widgets#index'});
  });
};

Support for _method

Since browsers don't support PUT or DELETE except in XHR, if exprestive receives a POST request with the _method variable set to PUT or DELETE, it should route appropriately.

Set @routes in middleware functions

@routes is set to the hash of reverse routes in controllers, but it would be great if middleware functions specified by controllers could have this bound to the controller instance so that @routes is available

Thoughts, @charlierudolph ?

Idea: make controllers simple functions

Right now we have imitated Rails' OO structure and require the controllers in Exprestive to be classes. I'm wondering if this is really necessary. Why can't we simplify them to:

module.exports = 

  index: (req, res) ->
      res.render 'users/index'

  new: (req, res) ->
    res.render 'users/new'

  ...

Making controllers classes is a misguided application of OO in my opinion. We don't really need independent objects here. There is no data and no behavior to encapsulate here. We don't need polymorphism. Unlike objects, controllers must be completely stateless and immutable. Summing this up, controllers are much more functional in nature than they are OO. They take a request as input, calculate a response (using external services), and send a response as output.

OO makes sense on the domain model level, but not for controllers or views.

Doing this also allows us to get rid of a requirement that the class name has to match the controller name. There is no more class name. The route foo#bar maps to the controller function keyed as bar in the file foo_controller.coffee.

@alexdavid @charlierudolph

Use verbatim routes files

The feature specs are more expressive if you provide direct examples for the systems under test.
Like this:

Given a routes file defining
  """
  GET /users   Users#index
  POST /users  Users#create
  """
And the following controller 
  """
  class UsersController

    index: (req, res) ->
      res.end 'hello world'
  """
When I make a request to "/users"
Then I receive a response with code: 200 and body: "hello world"

The multi-line strings are provided to you as a variable, similar to tables: http://cukes.info/step-definitions.html

You can use something like https://github.com/originate/kappamaki#attributes_from_sentence to parse complex sentences into a hash. Sorry no JS version yet, but should be trivial to build.

This makes it much easier to see the syntax of routes etc, and also to iterate it during development.

Support for scopes

module.exports = ({GET, scope}) ->
  scope '/abc', ->
    GET 'def', to: 'abc#def'
    GET 'ghi', to: 'abc#ghi'
    GET 'jkl', to: 'abc#jkl` 

equivalent to

module.exports = ({GET}) ->
  GET '/abc/def', to: 'abc#def'
  GET '/abc/ghi', to: 'abc#ghi'
  GET '/abc/jkl', to: 'abc#jkl` 

so the scope just gets prepended (with a trailing slash) to any routes defined under it

Make reverse route helpers return a non-primitive string with method attribute

@charlierudolph and I discussed #16 and came to the conclusion that while rails has magic around models, Exprestive should just be a routing library and therefore reverse routes need to be different for each action, and they should also return the method for form helpers to use.

This is the syntax we discussed:

  • paths.user(123) would still return a string to the reverse route /users/123
  • paths.user(123).method would return a string of the http method

This can be achieved with non-primitive strings:

str = new String '/users/123'
str.method = 'GET'

This way, as charlie mentioned, users of this library can easily implement a form helper:

//- form_helper.jade
mixin form(path)
  form(action=path, method=path.method)
    if block 
      block

//- index.jade
+form(paths.users(123))

@jondistad @kevgo please let me know what you think

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.