Configuring Requests
URL, query, and response formats for making requests.
Overview
This document describes how to configure can-connect for loading model data from a remote server. Every model in your application will have its own connection, and each connection will be able to Create, Read, Update, and Delete (CRUD) model data from the server. There are two sections of this document:
- Part 1 - Making requests - configuring your connection for outgoing requests
- Part 2 - Handling the response - parsing and formatting response data for use within your app
Part 1 - Making requests
When configuring a connection, the data-url behavior is responsible for making AJAX requests, and this behavior expects the URLs and data to be formatted a certain way. If your services do not line up with what is expected, we will describe how to configure things to meet your needs.
Note: all examples will be using the can-rest-model as it serves as a good starting point for learning how to use can-connect. For those familiar with
can.Model
, thecan-rest-model
is essentially the next generation.
Understanding the DataInterface
Each of the CRUD operations maps to one of the functions descibed by the DataInterface. The DataInterface
is a lower level interface used for loading raw data from a remote data source. This is not the same as the InstanceInterface which deals with typed data. Understanding the raw DataInterface
is crucial for making the customizations described below.
IMPORTANT: Whenever loading data within your application, you almost always want to use the InstanceInterface. The lower level
DataInterface
should be used for customizing how raw data is loaded .
getListData
- loads a list of records (with optional filtering, sorting, and pagination)getData
- loads an individual record by{id}
createData
- creates a new recordupdateData
- updates an existing record by{id}
destroyData
- deletes a record by{id}
Configuring the URL
When configuring a connection with the data-url behavior, a single URL can be used to describe all CRUD endpoints.
const connection = restModel({
url: "/api/todos/{id}"
});
The above is equivalent to the following long-hand configuration:
const connection = restModel({
url: {
getData: "GET /api/todos/{id}",
getListData: "GET /api/todos",
createData: "POST /api/todos",
updateData: "PUT /api/todos/{id}",
destroyData: "DELETE /api/todos/{id}",
}
});
Here is a working example you can play with in your browser:
import { restModel } from "can";
import { Todo, todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
// create mock data
todoFixture(5);
// define the connection
const connection = restModel({
url: "/api/todos/{id}",
ObjectType: Todo
});
// load data
connection.getList({}).then(todos => {
console.log(todos);
});
Note: For more information, read about the data-url behavior, the url configuration, and the DataInterface.
Customizing the request method and URL
Some applications will not follow the conventions expected by the data-url behavior. For example an application might only support GET
and POST
request methods or might use a unique URL structure. You can configure individual CRUD endpoints by defining the method and URL for each endpoint:
const connection = restModel({
url: {
getListData: 'GET /api/todos/all',
getData: 'GET /api/todo?uuid={id}',
createData: 'POST /api/todo/create',
updateData: 'POST /api/todo/update?uuid={id}',
destroyData: 'POST /api/todo/delete?uuid={id}',
}
});
Requests can be modified more extensively via the url
argument object than what is shown above. In particular, a beforeSend
hook function can be included. This function runs before a request is made, either modifying the outgoing request or performing additional tasks using the content of the request. See this and more in the full documentation for the url
argument here.
Implementing the DataInterface yourself
Consider a situation where an application loads all incomplete TODOs from a special URL like /api/todos/incomplete
. In such cases, you can write a custom getListData function which performs the actual ajax request. You can implement any of the DataInterface methods in a similar way:
import ajax from 'can-ajax';
const connection = restModel({
url: {
getListData(query) {
if(query.filter.complete === false) {
// Load all incomplete TODOs from a separate URL
return ajax({ url: '/api/todos/incomplete', data: query, /* ... */ });
}
// Load all other TODOs from the primary URL
return ajax({ url: '/api/todos', data: query, /* ... */ });
}
}
});
// Loads incomplete TODOs from '/api/todos/incomplete'
connection.getList({ complete: false });
// Loads all TODOs from '/api/todos'
connection.getList({});
Customizing the AJAX data transport
By default, can-ajax is used to make all data requests using XMLHttpRequest (XHR) and is based on the jQuery.ajax
interface. Any jQuery compatible transport can serve as a drop-in replacement for can-ajax
. Here is how you would use jQuery's ajax transport for all requests:
import $ from "jquery";
const connection = restModel({
url: "/api/todos/{id}",
ajax: $.ajax
});
Using another library for AJAX requests
You can create a thin wrapper to translate the ajaxOptions
expected by can-ajax into options for another library. For example, here is how you could use axios with can-connect:
import axios from 'axios';
function axiosTransport({ url, type, data, dataType }) {
const hasBody = /POST|PUT|PATCH/i.test(type);
// Must return a Promise
return axios({
url,
method: type,
params: hasBody ? {} : data,
data: hasBody ? data : {},
responseType: dataType || 'json',
}).then(res => {
// Must resolve to the parsed data object
return (typeof res === 'string') ? JSON.parse(res) : res;
});
}
const connection = restModel({
url: "/api/todos",
ajax: axiosTransport
});
Note: Any data transport can be used so long as it can conform to the can-ajax interface, which is based on the
jQuery.ajax
interface.
Creating your own data-url behavior
In really advanced situations, you can create your own custom data-url
behavior for making AJAX requests. You will need to implement the required DataInterface methods described above. Every method must return a Promise which resolves to the expected data:
connect.behavior("data/url", function( baseConnection ) {
return {
getListData(query) {
return ajax({
type: "GET",
url: this.url,
data: query
});
},
getData(query) { return ajax( /* ... */ ) },
createData(data) { return ajax( /* ... */ ) },
updateData(data) { return ajax( /* ... */ ) },
destroyData(data) { return ajax( /* ... */ ) }
};
});
Loading list data
Loading list data is unique because data can be filtered, sorted, and paginated using can-query-logic. This is where the real power of can-connect becomes available as it enables advanced behaviors such as the constructor store, real-time updates, caching, and other goodness. We recommend reading the following documents to become familiar with how to query logic works:
- The the comprehensive guide on can-query-logic
- Learn about the query structure
- Check out the available comparison operators used for filtering data
Part 2 - Parsing, formatting, and managing response data
Once raw data is loaded from a server, that data needs to be instantiated into typed data for use within your applicaiton. The constructor behavior is responsible for instantiating data returned from your services, and this behavior expects data to be formatted a certain way. If your services do not return data in the expected format, we will describe how to configure things to meet your needs.
Understanding how can-connect
manages Model instances
Many of the behaviors available with can-connect are designed to work with instances of Model data for your application. A single constructor/store is used to keep references to instance data and prevent multiple copies of an instance from being used by the application at once.
For example, if you have 3 different components which display information about the currently logged in User
, you can rest easy knowing that all 3 components will receive the same exact instance of that user. If the user gets updated by one component, all other components will receive those updates thanks to the real-time behavior.
Customizing how response data his handled
There are two types of response formats expected by the constructor behavior:
Instance Data: - The result of most CRUD operations. The full response body is treated as the instance data.
{ id: 111, name: 'Justin', email: ... }
List Data: The result of a a call to
getList
. An array of instance data must be on adata
property in the response body.{ count: 3, data: [ { id: 111, name: 'Justin', /* ... */}, { id: 222, name: 'Brian', /* ... */}, { id: 333, name: 'Paula', /* ... */} ] }
If your services return data in a format which is not expected, the following configurations can be used to customize how data is read and parsed from a response:
parseInstanceProp
- a custom property for looking up instance dataparseListProp
- a custom property for looking up list dataparseInstanceData
- a function for formatting response dataparseListData
- a function for formatting response data