diff --git a/lib/volt/controllers/restfull_base_controller.rb b/lib/volt/controllers/restfull_base_controller.rb
new file mode 100644
index 00000000..2cd9d666
--- /dev/null
+++ b/lib/volt/controllers/restfull_base_controller.rb
@@ -0,0 +1,63 @@
+require 'volt/controllers/http_controller'
+
+module Volt
+ # Allow you to create controllers that act as http endpoints
+ class RestfullBaseController < HttpController
+
+ before_action :setup_model
+ before_action :setup_new_resource, only: [:create]
+ before_action :setup_buffered_resource, only: [:update]
+ before_action :setup_resource, only: [:show, :destroy]
+
+ private
+
+ attr_reader :model, :resource
+
+ def self.model(model = :not_set)
+ if model == :not_set
+ @model
+ else
+ @model = model.to_sym
+ end
+ end
+
+ def collection_name
+ model.pluralize
+ end
+
+ def collection
+ store.send(collection_name)
+ end
+
+ def resource_params
+ params.send(:"_#{model}")
+ end
+
+ def setup_model
+ @model = self.class.model || params._model.try(:to_sym)
+ unless @model
+ render text: "No model given", status: :internal_server_error
+ stop_chain
+ end
+ end
+
+ def setup_new_resource
+ @resource = collection.new(resource_params)
+ end
+
+ def setup_resource
+ @resource = collection.where(id: params.id).first.sync
+ unless @resource
+ head :not_found
+ stop_chain
+ end
+ end
+
+ def setup_buffered_resource
+ setup_resource
+ @resource = @resource.buffer
+ end
+
+ end
+end
+
diff --git a/lib/volt/controllers/simple_json_controller.rb b/lib/volt/controllers/simple_json_controller.rb
new file mode 100644
index 00000000..9c321d3b
--- /dev/null
+++ b/lib/volt/controllers/simple_json_controller.rb
@@ -0,0 +1,42 @@
+require 'volt/controllers/restfull_base_controller'
+
+module Volt
+ # Allow you to create controllers that act as http endpoints
+ class SimpleJsonController < RestfullBaseController
+
+ def index
+ render json: { collection_name => collection.all.to_a.sync }
+ end
+
+ def show
+ render json: { model => resource.to_h }
+ end
+
+ def create
+ collection.append(resource).then do
+ # TODO http_controllers should be able to get routes via params
+ head :created #, location: params_to_url(:get, component: params._component, controller: params._controller, id: resource.id)
+ end.fail do |err|
+
+ end
+ end
+
+ def update
+ resource.update(resource_params)
+ resource.save!.then do
+ head :no_content
+ end.fail do
+
+ end
+ end
+
+ def destroy
+ resource.destroy.then do
+ head :no_content
+ end.fail do |err|
+
+ end
+ end
+
+ end
+end
diff --git a/lib/volt/volt/core.rb b/lib/volt/volt/core.rb
index a8c9bb15..c675fb10 100644
--- a/lib/volt/volt/core.rb
+++ b/lib/volt/volt/core.rb
@@ -1,3 +1,5 @@
# Require in the core volt classes that get used on the server
require 'volt/controllers/http_controller'
+require 'volt/controllers/restfull_base_controller'
+require 'volt/controllers/simple_json_controller'
require 'volt/server/rack/http_request'
diff --git a/spec/apps/kitchen_sink/app/api/config/dependencies.rb b/spec/apps/kitchen_sink/app/api/config/dependencies.rb
new file mode 100644
index 00000000..9c908a18
--- /dev/null
+++ b/spec/apps/kitchen_sink/app/api/config/dependencies.rb
@@ -0,0 +1,2 @@
+# Specify which components you wish to include when
+# this component loads.
diff --git a/spec/apps/kitchen_sink/app/api/config/initializers/boot.rb b/spec/apps/kitchen_sink/app/api/config/initializers/boot.rb
new file mode 100644
index 00000000..a4bbae35
--- /dev/null
+++ b/spec/apps/kitchen_sink/app/api/config/initializers/boot.rb
@@ -0,0 +1,10 @@
+# Place any code you want to run when the component is included on the client
+# or server.
+
+# To include code only on the client use:
+# if RUBY_PLATFORM == 'opal'
+#
+# To include code only on the server, use:
+# unless RUBY_PLATFORM == 'opal'
+# ^^ this will not send compile in code in the conditional to the client.
+# ^^ this include code required in the conditional.
\ No newline at end of file
diff --git a/spec/apps/kitchen_sink/app/api/config/routes.rb b/spec/apps/kitchen_sink/app/api/config/routes.rb
new file mode 100644
index 00000000..6b4b3e87
--- /dev/null
+++ b/spec/apps/kitchen_sink/app/api/config/routes.rb
@@ -0,0 +1 @@
+# See https://github.com/voltrb/volt#routes for more info on routes
diff --git a/spec/apps/kitchen_sink/app/api/controllers/main_controller.rb b/spec/apps/kitchen_sink/app/api/controllers/main_controller.rb
new file mode 100644
index 00000000..cb394f4f
--- /dev/null
+++ b/spec/apps/kitchen_sink/app/api/controllers/main_controller.rb
@@ -0,0 +1,20 @@
+module Api
+ class MainController < Volt::ModelController
+ def index
+ # Add code for when the index view is loaded
+ end
+
+ def about
+ # Add code for when the about view is loaded
+ end
+
+ private
+
+ # the main template contains a #template binding that shows another
+ # template. This is the path to that template. It may change based
+ # on the params._controller and params._action values.
+ def main_path
+ "#{params._component || 'main'}/#{params._controller || 'main'}/#{params._action || 'index'}"
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/apps/kitchen_sink/app/api/controllers/server/api_controller.rb b/spec/apps/kitchen_sink/app/api/controllers/server/api_controller.rb
new file mode 100644
index 00000000..9fd92331
--- /dev/null
+++ b/spec/apps/kitchen_sink/app/api/controllers/server/api_controller.rb
@@ -0,0 +1,5 @@
+module Api
+ class ApiController < Volt::SimpleJsonController
+
+ end
+end
diff --git a/spec/apps/kitchen_sink/app/api/views/main/index.html b/spec/apps/kitchen_sink/app/api/views/main/index.html
new file mode 100644
index 00000000..da57fc22
--- /dev/null
+++ b/spec/apps/kitchen_sink/app/api/views/main/index.html
@@ -0,0 +1,5 @@
+<:Title>
+ Component Index
+
+<:Body>
+
Component Index
\ No newline at end of file
diff --git a/spec/apps/kitchen_sink/app/main/config/routes.rb b/spec/apps/kitchen_sink/app/main/config/routes.rb
index 99b28935..fc49441e 100644
--- a/spec/apps/kitchen_sink/app/main/config/routes.rb
+++ b/spec/apps/kitchen_sink/app/main/config/routes.rb
@@ -34,5 +34,8 @@
# Route for file uploads
client '/upload', controller: 'upload', action: 'index'
+# Simple JSON API specs
+rest '/api/issues', component: 'api', controller: 'api', model: 'issue'
+
# The main route, this should be last. It will match any params not previously matched.
client '/', {}
diff --git a/spec/apps/kitchen_sink/app/main/models/issue.rb b/spec/apps/kitchen_sink/app/main/models/issue.rb
new file mode 100644
index 00000000..a2c95855
--- /dev/null
+++ b/spec/apps/kitchen_sink/app/main/models/issue.rb
@@ -0,0 +1,3 @@
+class Issue < Volt::Model
+ field :name, String
+end
\ No newline at end of file
diff --git a/spec/controllers/restfull_base_controller_spec.rb b/spec/controllers/restfull_base_controller_spec.rb
new file mode 100644
index 00000000..abf7d72d
--- /dev/null
+++ b/spec/controllers/restfull_base_controller_spec.rb
@@ -0,0 +1,171 @@
+require 'spec_helper'
+
+if RUBY_PLATFORM != 'opal'
+ require 'volt/controllers/restfull_base_controller'
+ require 'volt/server/rack/http_request'
+ require 'volt/server/rack/http_resource'
+
+ describe Volt::RestfullBaseController do
+ class TestRestfulController < Volt::RestfullBaseController
+
+ attr_accessor :the_model, :the_collection, :the_collection_name, :the_resource_params
+
+ def model_test
+ self.the_model = model
+ end
+
+ def collection_test
+ self.the_collection = collection
+ self.the_collection_name = collection_name
+ end
+
+ def params_test
+ self.the_resource_params = resource_params
+ end
+
+ end
+
+ class StaticRestfulController < Volt::RestfullBaseController
+ attr_accessor :the_model
+
+ model :issue
+
+ def model_test
+ self.the_model = model
+ end
+ end
+
+ class ImplementedRestfulController < Volt::RestfullBaseController
+ attr_accessor :the_resource
+
+ def create
+ self.the_resource = resource
+ end
+
+ def update
+ self.the_resource = resource
+ end
+
+ def show
+ self.the_resource = resource
+ end
+
+ def destroy
+ self.the_resource = resource
+ end
+ end
+
+ let(:app) { ->(env) { [404, env, 'app'] } }
+
+ def request(url='http://example.com/issues')
+ Volt::HttpRequest.new(
+ Rack::MockRequest.env_for(url, 'CONTENT_TYPE' => 'application/json;charset=utf-8'))
+ end
+
+
+ let(:controller) { TestRestfulController.new(volt_app, {}, request) }
+
+ before(:each) do
+ store.issues.reverse.each do |issue|
+ issue.destroy
+ end
+ end
+
+ it 'should set the model from the params' do
+ controller = TestRestfulController.new(
+ volt_app, {model: 'issue'}, request
+ )
+ controller.perform(:model_test)
+ expect(controller.the_model).to eq(:issue)
+ end
+
+ it 'should use a static model if set' do
+ controller = StaticRestfulController.new(
+ volt_app, {model: 'none'}, request
+ )
+ controller.perform(:model_test)
+ expect(controller.the_model).to eq(:issue)
+ end
+
+ it 'should set the collection based on the model name' do
+ controller = TestRestfulController.new(
+ volt_app, {model: 'issue'}, request)
+
+ controller.perform(:collection_test)
+ expect(controller.the_collection_name).to eq(:issues)
+ expect(controller.the_collection).to be_kind_of(Volt::ArrayModel)
+ expect(controller.the_collection.new).to be_kind_of(Issue)
+ end
+
+ it 'should set the correct resource_params based on the model name' do
+ issue = { name: 'test' }
+ controller = TestRestfulController.new(
+ volt_app, {model: 'issue', issue: issue }, request
+ )
+ controller.perform(:params_test)
+ expect(controller.the_resource_params).to eq(issue)
+ end
+
+ it 'should setup a new instance of a model with the given params for the create action' do
+ issue = { name: 'test' }
+ controller = ImplementedRestfulController.new(
+ volt_app, {model: 'issue', issue: issue }, request
+ )
+ controller.perform(:create)
+ expect(controller.the_resource).to be_kind_of(Issue)
+ expect(controller.the_resource.root).to be_kind_of(Issue)
+ new_issue = controller.the_resource.to_h
+ new_issue.delete(:id)
+ expect(new_issue).to eq(issue)
+ end
+
+ it 'should setup a buffer of a model for the update action' do
+ issue = store.issues.create({ name: 'test' }).sync
+ controller = ImplementedRestfulController.new(
+ volt_app, { model: 'issue', id: issue.id }, request
+ )
+ controller.perform(:update)
+ expect(controller.the_resource.to_h).to eq(issue.to_h)
+ expect(controller.the_resource.root).to eq(store)
+ expect(controller.the_resource.buffer?).to be(true)
+ end
+
+ it 'should setup the model for the show action' do
+ issue = store.issues.create({ name: 'test' }).sync
+ controller = ImplementedRestfulController.new(
+ volt_app, { model: 'issue', id: issue.id }, request
+ )
+ controller.perform(:show)
+ expect(controller.the_resource.to_h).to eq(issue.to_h)
+ expect(controller.the_resource.root).to eq(store)
+ expect(controller.the_resource.buffer?).to be(false)
+ end
+
+ it 'should setup the model for the delete action' do
+ issue = store.issues.create({ name: 'test' }).sync
+ controller = ImplementedRestfulController.new(
+ volt_app, { model: 'issue', id: issue.id }, request
+ )
+ controller.perform(:show)
+ expect(controller.the_resource.to_h).to eq(issue.to_h)
+ expect(controller.the_resource.root).to eq(store)
+ expect(controller.the_resource.buffer?).to be(false)
+ end
+
+ it 'should respond with http 404 not found if the resource could not be found' do
+ controller = ImplementedRestfulController.new(
+ volt_app, { model: 'issue', id: 0 }, request
+ )
+ response = controller.perform(:show)
+ expect(response.status).to eq(404)
+ end
+
+ it 'should respond with http 500 internal server error no model is set' do
+ controller = ImplementedRestfulController.new(
+ volt_app, { }, request
+ )
+ response = controller.perform(:show)
+ expect(response.status).to eq(500)
+ end
+ end
+end
diff --git a/spec/controllers/simple_json_controller_spec.rb b/spec/controllers/simple_json_controller_spec.rb
new file mode 100644
index 00000000..dc29d6ae
--- /dev/null
+++ b/spec/controllers/simple_json_controller_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+if RUBY_PLATFORM != 'opal'
+ describe "SimpleJsonApiController" do
+ include Rack::Test::Methods
+
+ let(:app) { Volt.current_app.middleware }
+
+ def json_response
+ JSON.parse(last_response.body, symbolize_names: true)
+ end
+
+ def headers
+ { 'CONTENT_TYPE' => 'application/json' }
+ end
+
+ before(:each) do
+ store.issues.reverse.each do |issue|
+ issue.destroy
+ end
+ end
+
+ it 'should return a list of resources as json' do
+ store.issues << Issue.new(name: 'test')
+ get '/api/issues'
+ expect(last_response.status).to eq(200)
+ expect(last_response.headers['Content-Type']).to eq('application/json')
+ expect(json_response).to eq(issues: store.issues)
+ end
+
+ it 'should returns a single resource as json' do
+ store.issues << Issue.new(name: 'first')
+ issue = store.issues.create(name: 'test').sync
+ get '/api/issues/' + issue.id
+ expect(last_response.status).to eq(200)
+ expect(last_response.headers['Content-Type']).to eq('application/json')
+ expect(json_response).to eq(issue: issue.to_h)
+ end
+
+ it 'should respond with 404 if the resource can not be found' do
+ get '/api/issues/0'
+ expect(last_response.status).to eq(404)
+ end
+
+ it 'destroys a resource' do
+ first = store.issues.create(name: 'first').sync
+ issue = store.issues.create(name: 'second').sync
+ delete '/api/issues/' + issue.id
+ expect(last_response.status).to eq(204)
+ expect(store.issues.to_a.sync).to eq([first.to_h])
+ end
+
+ it 'creates a resource' do
+ payload = JSON.dump({ issue: {name: 'a issue' } })
+ expect(store.issues.count.sync).to eq(0)
+ post '/api/issues', payload, headers
+ puts last_response.body
+ expect(last_response.status).to eq(201)
+ expect(store.issues.count.sync).to eq(1)
+ issue = store.issues.first.sync
+ expect(last_response.body).to be_empty
+ expect(issue.name).to eq('a issue')
+ # TODO http_controllers should be able to get routes via params
+ #expect(last_response.header['Location']).to match("/api/issues/" + issue.id)
+ end
+
+ it 'updates a resource' do
+ issue = store.issues.create(name: 'test').sync
+ payload = JSON.dump({ issue: {name: 'new name' } })
+ put '/api/issues/' + issue.id, payload, headers
+ puts last_response.body
+ expect(last_response.status).to eq(204)
+ issue = store.issues.first.sync
+ expect(issue.name).to eq 'new name'
+ end
+ end
+end