Bundle endpoints as callable, reusable units
Axios Actions comprises a small set of classes which allows you to collate and call URLs as actions:
const collection = new <ApiClass>(axios, actions)
collection
.<action>(<data>)
.then(<handler>)
This collection-based approach allows you to set up APIs without scattering URLs and handler code throughout your application; you set up your endpoints once, then import and use these robust, functional units where needed.
The end result is a significant reduction in application complexity (especially where replacing the Flux pattern) but without sacrificing separation of concerns.
The ApiGroup
class is the base building block for actions.
You set it up with an axios instance and a configuration block:
export const widgets = new ApiGroup(axios, {
view: 'api/products/widgets?category=:category',
load: 'api/products/widgets/:id',
save: 'POST api/products/widgets/:id',
})
Note that:
- HTTP methods can be specified directly in the URL format
- placeholder variables are automatically filled-in using passed data
- placeholders can be used anywhere in the string
- You can use
:value
or{value}
style placeholders
To use the endpoint, import it, then either call()
the action by name, or execute it directly:
import { widgets } from '../api'
// call by name
widgets.call('load', 1).then(onLoad)
// execute directly
widgets.load(1).then(onLoad)
Note that ApiGroup
will not override existing properties or methods with new ones.
The ApiEndpoint
class extends ApiGroup
to automatically set up REST verbs, paths and CRUD actions:
const posts = new ApiEndpoint(axios, 'posts/:id')
This makes it even easier to call REST endpoints as methods are declared on the class:
posts.index() // GET
posts.create(data) // POST
posts.read(1) // GET
posts.update(data) // PATCH
posts.delete(1) // DELETE
If your endpoints are not strictly REST, pass in a configuration object instead:
const posts = new ApiEndpoint(axios, {
index: 'posts/index', // GET
create: 'posts/create', // POST
read: 'posts/view/:id', // GET
update: 'posts/update/:id', // POST
delete: 'posts/delete/:id' // POST
})
For object configuration, the class is configured to use GET
for index
/ read
and POST
for everything else. If you want to override these defaults, indicate the correct HTTP method within the URL string:
{ update: 'PATCH posts/update/:id' }
The ApiResource
class extends ApiEndpoint
and is supplied primarily to demonstrate the package's core features working together in a single class.
In addition to
ApiCore
features such asloading
state anderror
messagesApiGroup
features such as actions andwhen()
handlersApiEndpoint
actionscreate
,read
,update
,delete
andindex
it:
- provides an additional
search
action - saves
items
anditem
data on the instance itself - uses the
resource
plugin to optionally convert to and from a supplied Model - uses the
when
handler to optionally reloadindex
on successfulcreate
,update
anddelete
operations
The class is designed to encapsulate all API-related functionality in a single place, making it as simple as possible to call endpoints and update your UI without leaking logic or data into composing components.
The following is a Vue example:
data () {
return {
api: new ApiResource(axios, 'comments/:id', Comment, true)
}
}
<button @click="api.index()">Load</button>
<section :class="{ loading: api.loading }">
<comment v-if="api.items" v-for="model in api.items" v-bind="model" />
<error v-if="api.error" />
</section>
Note that there are no component-level variables such as loading
, error
, data
or handlers for any action or subsequent assignment, for example:
// x 6 actions
create (data) {
this.api
.create(data)
.then(res => this.data = res.data)
.catch(error => this.error = error.message)
}
Check the demo online to see it in action:
Its implementation should cover a broad range of use cases, but where it doesn't (for example adding additional pagination methods) it's a simple case of extending the class or even duplicating the original source and creating your own ApiResource
class.
All classes extend from the ApiCore
class which contains base functionality for:
- HTTP methods
get()
,post()
andrequest()
- event handling via
done()
andfail()
- loading state via
loading
- error state via
error
- plugins via
use()
import { ApiCore } from 'axios-endpoints'
// create new Api instance
this.api = new ApiCore(axios)
// return data not repsonse
.use('data')
// add global handlers
.done(data => this.comments = data)
.fail(error => this.error = this.api.error.message)
// call an endpoint directly
this.api.get('api/comments')
In your application's view, you can sync with instance properties (for any of the classes) to show progress and updates:
<error v-if="api.error">{{ api.error }}</error>
<loading v-else-if="api.loading" />
<div v-else>
<comment v-for="commment in comments" :model="comment" />
</div>
Note that each Api class instance monitors its own loading progress, and will report loaded only when all requests have loaded. This way you can have separate, lightweight Api instances for individual components or sections of your site.
See the class itself for all methods:
The package acts as a layer on top of your existing axios setup, so things like base URL, authentication and errors would be handled in axios, then the axios instance passed to Axios Actions classes:
import axios from 'axios'
axios.defaults.baseURL = '...'
axios.interceptors.request.use( ... )
axios.interceptors.response.use( ... )
const endpoint = new ApiEndpoint(axios, config)
Any of the main classes can have actions added to them at any point:
const comments = new ApiEndpoint('comments/:id')
comments.add('search', 'comments/search?user=:userId&text=:text')
Again, prepend the HTTP method to the front of the URL if you need to, then execute via call()
or the automatically-created method:
comments
.search(form)
.then(onSearch)
The method supports an alternative long-hand signature where by you can manually specify the HTTP method and also an action-level handler, which is called only on a successful call to the URL endpoint:
comments.add(action, path, method, handler)
Note that a method in the path will always override a method passed as an argument.
Actions can be added to any ApiGroup
instances, or if greater functionality is required, you can extend from a base class and add your own custom methods.
Events can be handled in three ways:
- per call
- per group of actions
- per action
To set up call-level event handling, use done()
and fail()
on the Api instance:
const posts = new ApiEndpoint('posts/:id')
.done(onLoad)
.fail(onError)
posts.index()
To set up group-level event handling, use when()
on any ApiGroup
(or subclass) instance:
const posts = new ApiEndpoint('posts/:id')
.when('create update delete', onAction)
function onAction (res, action) {
console.log(`action: ${action}`, res)
posts.index() // reload
}
posts.create({title: 'new post', body: 'this is a new post'})
Note that the handler will only be called for successful calls.
To set up action-level event handling, use then()
and catch()
on the call return:
const posts = new ApiEndpoint('posts/:id')
posts
.index()
.then(res => this.data = res.data.map(post => new Post(post)))
The Http
class which manages the Axios calls allows you to modify the outgoing data and incoming responses in a similar manner to Axios intercepters, via before
and after
arrays.
To modify data or response, add handler functions:
endpoint.http.before.push(data => onRequest)
endpoint.http.after.push(data => onResponse)
You can import the process
helper function to abstract the response data whether it's an array or a single object:
import { helpers } from 'axios-actions'
function onResponse (res) {
helpers.process(res.data => item => {
Object
.keys(item)
.forEach(key => {
item[key] = String(item).toUpperCase()
})
})
}
Plugins wrap up the kind of functionality above into easy-to-use functions that can be called directly, or added by name to all Api classes.
There are currently three plugins:
resource
- converts outgoing and incoming data to and from modelsremap
- remaps key names from client to server and back againdata
- returns the data component only from any calls
To use plugins, use the use()
method of any of the Api classes:
const endpoint = new ApiEndpoint(axios, config)
.use('resource', Post)
.use('remap', { the_title: 'title'}, the_body: 'body')
.use('data')
See the plugins file itself for all built-in plugin functions:
Very simply, plugins are functions which take any Api instance, then any parameters you want to pass:
function doSomething (api, foo, bar) { ... }
To extend the uppercase example from the previous section, we might do the following:
import { helpers } from 'axios-actions'
// create plugin
function changeCase (api, state) {
const transform = state ? 'toUpperCase': 'toLowerCase'
const onResponse = function (res) {
helpers.process(res.data => item => {
Object
.keys(item)
.forEach(key => {
item[key] = String(item)[transform]()
})
})
}
api.http.after.push(res => onResponse)
}
To implement it in our project, we can do the following:
// add to global plugins
import { plugins } from 'axios-actions'
plugins.changeCase = changeCase
// implement via use()
const posts = new ApiEndpoint('posts/:id')
.use('changeCase', true)
// implement via calling
changeCase(posts, false)
You can do anything you like with the api
instance in the plugin, for example adding callbacks:
plugins.log = function (api) {
api.fail(error => {
console.log('ERROR!', error)
return Promise.reject(error)
})
}
A typical use case for Axios Actions is also to extend an existing class with your own methods.
The following example shows how to decouple your API from your Flux-based (Vuex) store, using it only for shared data:
import axios from 'axios'
import { ApiEndpoint } from 'axios-actions'
import store from './store'
class VuexResource extends ApiEndpoint {
constructor (path, mutation) {
super(axios, path)
this
.when('create update delete', () => this.index())
.when('index', data => store.commit(mutation, data))
.use('data')
}
}
const posts = new VuexResource('posts/:id', 'posts/data')
posts.index()
posts.create({ ... }) // no handlers needed; new data loads in automatically!
Check the demo folder for local, live, and editable demos:
Install via NPM:
npm install axios-actions