I am building a RESTful API with Yii2 but have some questions in regards to HATEOAS support. Requests will output the pagination headers and include the HATEOAS header.
However the HATEOAS header contains all links as one long string. This is not very helpful for the consumer. Is this standard? Is there a way to change the format in Yii into something that is easier to handle?
Does the following look good?
"_links": {
"self": {
"href": "http://localhost/users?page=1"
},
"next": {
"href": "http://localhost/users?page=2"
},
"last": {
"href": "http://localhost/users?page=50"
}
}
If so, you can easily have links like that. Make sure your data model implements Linkable interface and then implement getLinks() method:
class User extends ActiveRecord implements Linkable
{
public function getLinks()
{
return [
Link::REL_SELF => Url::to(['user/view', 'id' => $this->id], true),
];
}
}
Serializer will automatically add "_links" to your response.
More info here.
Multiple HTTP headers with the same name can be combined by comma-separating them.
The easiest way to get access to each individual link is to use a HTTP Link header parser library, any of which will for sure already have support multiple comma-separated header values.
It's very important that any client that consumes this supports both a HTTP Header appearing multiple times, and the comma-separated syntax because an intermediate (like a proxy, load balancer, CDN) might change the many-header to combined-header syntax. A good client supports both.
Related
There are tons of articles, blogs and API docs about REST API resource field expansion but none about how to implement an expansion in aspect of technique and data query in right way.
Simple example for a flat resource response:
GET /api/v1/customers
{
data: [
{
"id": "209e5d80-c459-4d08-b53d-71d11e216a5d",
"contracts": null
},
{
"id": "c641cb83-af29-485d-9be2-97925057e3b2",
"contracts": null
}
],
expandable: ["contract"]
}
Simple example for expanded resource:
GET /api/v1/customers?expand=contract
{
data: [
{
"id": "209e5d80-c459-4d08-b53d-71d11e216a5d",
"contracts": [
{......},
{......},
{......},
]
},
{
"id": "c641cb83-af29-485d-9be2-97925057e3b2",
"contracts": [
{......},
{......},
{......},
]
}
],
expandable: ["contract"]
}
Lets assume we use a api rest controller class which handles the enpoints and a read service (maybe cqs/cqrs based) which uses plain sql for read performance. At which point does the expansion logic start and what is the right way of handling queries without an exponential increase in queries?
Afaik, this is not possible in one or few SQL queries (except the dirty way of GROUP_CONCAT and separation of all data into one field). Should I query all customers and then iterate over all customers and query expanded data for each customer? This would cause an exponential increase of queries.
I was looking for the same thing and I would say that the correct answer is it depends on the infrastructure used.
In a general manner, the implementation should receive the expand or expandable' object in your endpoint and respond acordingly. So, if we have something like your example, when requesting /api/v1/customersviaGETpassing theexpandableobject, you should run another query to the database to get, as in the example, theuser contracts`.
There is not a unique, one size fits all answer for this question, specially if we are not talking about a specific language + framework.
A common way of passing parameters to a RESTful web service is in the URL:
website.com/action.php?table=myTable&key=myKey&values=myValues
Another way would be with JSON:
{
"data":
[
{
"parameters": {"table":"myTable", "key":"myKey", "values":"myValues"}
},
{
"content": {"data1":"dataVal1","data2":"dataVal2"}
}
]
}
What would be the pros and cons of these two methods:
When would I use one over the other
Benefits of each one
Weaknesses
Performance differences
To retrieve values using REST, you must use a GET request. There are no request bodies with GET requests, so your only option is the url.
When changing values in REST service, you typically use the PUT request. This PUT request should contain the new resource state in it's body.
So it's not an either/or matter. Where you place certain request parameters depends on what kind of operation you are doing, and what the meaning of the parameter is.
I've been looking into GraphQL as a replacement for some REST APIs of mine, and while I think I've wrapped my head around the basics and like most of what I see so far, there's one important feature that seems to be missing.
Let's say I've got a collection of items like this:
{
"id": "aaa",
"name": "Item 1",
...
}
An application needs a map of all those objects, indexed by ID as such:
{
"allItems": {
"aaa": {
"name": "Item 1",
...
},
"aab": {
"name": "Item 2",
...
}
}
}
Every API I've ever written has been able to give results back in a format like this, but I'm struggling to find a way to do it with GraphQL. I keep running across issue 101, but that deals more with unknown schemas. In my case, I know exactly what all the fields are; this is purely about output format. I know I could simply return all the items in an array and reformat it client-side, but that seems like overkill given that it's never been needed in the past, and would make GraphQL feel like a step backwards. I'm not sure if what I'm trying to do is impossible, or I'm just using all the wrong terminology. Should I keep digging, or is GraphQL just not suited to my needs? If this is possible, what might a query look like to retrieve data like this?
I'm currently working with graphql-php on the server, but I'm open to higher-level conceptual responses.
Unfortunately returning objects with arbitrary and dynamic keys like this is not really a first-class citizen in GraphQL. That is not to say you can't achieve the same thing, but in doing so you will lose many of the benefits of GraphQL.
If you are set on returning an object with id keys instead of returning a collection/list of objects containing the ids and then doing the transformation on the client then you can create a special GraphQLScalarType.
const GraphQLAnyObject = new GraphQLScalarType({
name: 'AnyObject',
description: 'Any JSON object. This type bypasses type checking.',
serialize: value => {
return value;
},
parseValue: value => {
return value;
},
parseLiteral: ast => {
if (ast.kind !== Kind.OBJECT) {
throw new GraphQLError("Query error: Can only parse object but got a: " + ast.kind, [ast]);
}
return ast.value;
}
});
The problem with this approach is that since it is a scalar type you cannot supply a selection set to query it. E.G. if you had a type
type MyType implements Node {
id: ID!
myKeyedCollection: AnyObject
}
Then you would only be able to query it like so
query {
getMyType(id: abc) {
myKeyedCollection # note there is no { ... }
}
}
As others have said, I wouldn't recommend this because you are losing a lot of the benefits of GraphQL but it goes to show that GraphQL can still do pretty much anything REST can.
Hope this helps!
I've being building an API and I find myself in a situation where I want to reuse the models from the API in my webapp.
[query]<>[webapp]<>[models]<>[api]<>[models]<>[db]
In the webapp I'm creating models based on the output of the API.
In the API I'm creating models based on the output of the database.
Both models are identical to each other.
Is there a way I can do this properly without copy/pasting the models?
Or is there a way to avoid having the same models?
EDIT:
What I mean is that I'm using a PHP-API and a PHP-Frontend.
Both models in PHP-API and PHP-Frontend are equal to each other.
You could make a service within the API that returns the model data as its response. That way you can build a simple converter to change the models from PHP into some kind of JSON or other data structure format in the backend and transmit them.
You'll only have one copy of the actual models and will auto-generate the ones used by the frontend.
A very simple example to elaborate:
class Backend_Book {
public $title;
public $author;
}
class Backend_Recipe {
public $name;
public $ingredients;
public $steps;
}
Add to your api:
class Api_Get_Domain {
$domain = load_domain();
$json = [];
foreach( $domain as $domain_model ) {
$json[] = convert_model($domain_model);
}
return json_encode($json);
}
Call from your frontend:
$http.get('your/api/domain').then( function( response ) {
domain = response;
});
And then the idea is that "response" will be something like this:
response = [
{
model: "Book",
properties: [
"title", "author"
]
},
{
model: "Recipe",
properties: [
"name", "ingredients", "steps"
]
}
];
And now you have, in your frontend, a copy of the domain as used by the backend, that you won't need to update. Obviously this is a very basic example, you could add as much information to the domain models as you need, but the basic idea is to make the API provide its own domain to the users of the API.
I'm currently working on building an API with the Symfony framwork. I've done enough reading to know to use the Serialization component, and built some custom normalizers for my entities. The way it currently works is:
JSON -> Array(Decode) -> User Entity(Denormalize)
This was working find as long as the request content was a JSON representation of the user, example:
{
"email": "demouser#email.com",
"plainPassword": "demouser",
"first_name" : "Demo",
"last_name" : "User"
}
A user entity is created using the following code in my controller:
$newuser = $this->get('api.serializer.default')->deserialize($request->getContent(), WebsiteUser::class, 'json');
However, I'd like to nest the user JSON in the 'data' property of a JSON object, which will allow consumers to pass additional metadata with the request, example:
{
"options": [
{
"foo": "bar"
}
],
"data": [
{
"email": "demouser#email.com",
"plainPassword": "demouser",
"first_name": "Demo",
"last_name": "User"
}
]
}
The main issue this causes is that the deserialization does not succeed because the JSON format has changed.
The only solution I've considered so far is to json_decode the whole request body, grab the 'data' element of that array, and pass the contents of the data element to the denormalizer (instead of the deserializer).
Is there a better way to solve this problem?
You should be able to get a specific key of your request body like follows:
$newuser = $this->get('api.serializer.default')->deserialize(
$request->request->get('data'), WebsiteUser::class, 'json'
);
If you are not able to retrieve the data from key without decoding your request body, look at this bundle, it consists in only one EventListener that replaces the request body after decode it.
You can easily integrate the same logic in your application, or requiring the bundle directly (which works well).