35

I'm looking for the best practices on RESTful API design for the following use case:

Domain Object Vehicle:

class Vehicle {
    private String vehicleType;
    private String colour;
    private String transmission;
    private String yearOfIssue;
}

An example object:

Vehicle = {vehicleType : 'Car', colour : 'Red', transmission : 'Automatic', yearOfIssue : '2008'};

In this domain model, there is no single field unique identifier (e.g. vehicleId), but rather all fields of the object together form the primary key (this constraint is there in the database layer).

We have no flexibility to alter this domain model to add a single field unique identifier.

So my question is as follows - If I want to add a simple REST API on top of this domain object that provides simple functionality to CREATE, UPDATE, DELETE and GET Vehicles, what is the best practice for the PATH endpoints for these methods?

Following the above example, if the domain model were to have a single field unique identifier vehicleId, then I can imagine the following endpoints:

GET /vehicles/:vehicleId
PUT /vehicles/:vehicleId
DELETE /vehicles/:vehicleId

I'm not aware of a pattern that exists similar to this for composite keys as:

GET /vehicles/:vehicleTypecolourtransmissionyearOfIssue
GET /vehicles/CarRedAutomatic2008

seems incorrect.

Any advice on a good pattern to follow for this use case would be greatly appreciated.

Thanks

JoshDavies
  • 727
  • 2
  • 8
  • 12

3 Answers3

19

As per general REST standards, each endpoint exposes a resource, and client can work on them with http verbs. In this example your resource is vehicle, and client is fetching data from server using GET. Ideally, each resource should be uniquely identified with a unique (single) key.

But your resource (vehicle) does not have a single value unique key, and it cannot be changed in the system! In this case you can still make the GET call with all required parameters to identify the resource, like any other standard http calls, like

GET /vehicles?type=Car&color=Red&transmission=Automatic&manufactureYear=2008

The technology/platform you are using, if that allows making custom routes for your method, you can create a custom route something like

new route("/vehicles/{type}/{color}/{transmission}/{manufactureYear}")

And call your service as

GET /vehicles/Car/Red/Automatic/2008

The good thing about this is, your uri becomes shorter. But on the other hand [1] For all methods/resources of this type, you'll have to create custom routes, and [2] this uri doesn't make much sense unless you have knowledge of the specific method and route.

Arghya C
  • 9,805
  • 2
  • 47
  • 66
  • 7
    Using path is better than params to specify the key, because it works with any of the verbs. FOr instance you can update using `POST /vehicles/Car/Red/Automatic/2008` – John Henckel Feb 18 '16 at 18:09
  • 4
    @JohnHenckel according to http://tools.ietf.org/html/rfc3986#section-3.4, the query parameters are part of the URL and as such you should be able to POST to them as well. see https://stackoverflow.com/a/20637055/214446 – mb21 Nov 06 '17 at 13:36
  • 3
    I'd like to see the bigger question resolved: can the OP's task be done using REST, or is it just a standard HTTP Get? I'm going to guess that without a single, unique identifier, it's not REST. – Quark Soup Oct 08 '18 at 15:51
  • @DonaldAirey see my answer for something like a single unique identifier. – bugged87 Dec 04 '18 at 00:44
11

In ASP.NET Core, I typically represent a composite key like this:

POST /vehicles/(car:red:automatic:2008)

or

POST /vehicles/(car|red|automatic|2008)

The framework has no problem mapping these to action parameters in the specified order.

[HttpPut("vehicles/({car}:{color}:{trans}:{year})")]
public async Task<IActionResult> Add(
    string car, string color, string trans, int year, [FromBody] Vehicle request)
{
    await Task.CompletedTask;

    return Ok();
}

Example request: PUT /vehicles/(Ford:Ranger:100% genuine:2000)

enter image description here

bugged87
  • 3,026
  • 2
  • 26
  • 42
  • 2
    This is a weak solution because it assumes that the data does not contain any special characters. For example, someone insert a car with a transmission called "100% genuine" then ASP.NET will throw an exception. It does not allow special characters in the path. – John Henckel Dec 03 '18 at 18:35
  • 3
    @JohnHenckel this answer is specifically for `ASP.NET Core` (as stated) and that framework allows special characters just fine. See the screenshot added to the answer. – bugged87 Dec 04 '18 at 00:33
  • This is what I went for, because I think using the query parameters is meant for the endpoint to return a collection rather than an unique resource. I ended up using just dot (.) to separate them, like `/vehicles/car.red.automatic.2008` – David Liang Aug 18 '22 at 23:05
0

To be RESTful, you would need to create a single, unique identifier to augment your class.

class Vehicle {
    public int vechicleId { get; set; }
    public string vehicleType { get; set; }
    public string colour { get; set; }
    public string transmission { get; set; }
    public string yearOfIssue { get; set; }
}

Then you could access it using HTTP:Get http://mysite/vehicles/3842. However, you may not have access to the internal, unique identifier, especially when you seed or update the database. I've run across similar issues and to use the REST verbs I'll include an external identifier to make it easier for humans and external systems to access the records:

class Vehicle {
    public int vechicleId { get; set; }
    public string externalId { get; set; }
    public string vehicleType { get; set; }
    public string colour { get; set; }
    public string transmission { get; set; }
    public string yearOfIssue { get; set; }
}

Then the verb looks like: HTTP:Get http://mysite/vehicles/externalId/sedanbluemanual2015. You don't have to parse the URI as all that data should be in the body of the message, you just need to make sure that the string uniquely identifies the vehicle.

[HttpPut("externalId/{externalId}")]
public IActionResult PutVehicle([FromRoute] string externalId, [FromBody] JObject jObject)
{
    // See if the record exists already.
    var oldVehicle = (from v in vehicles
                       where vehicle.ExternalId == externalId
                       select v).FirstOrDefault();
    if (oldVehicle != null)
    {
        <insert new vehicle>
    }
    else
    {
        <update old vehicle>
    }
Quark Soup
  • 4,272
  • 3
  • 42
  • 74
  • 1
    I know this is an old question/answer, but can you cite a specification on "To be RESTful, you would need to create a single, unique identifier to augment your class." As far as I can tell from research, there is no such requirement stated anywhere authoritatively. Composite resource IDs are everywhere. – Luke Oct 25 '22 at 19:59
  • The whole basis of REST is being able to identify a resource with a URI. I would argue that it's self-evident that this URI must be unique to unambiguously identify a resource. If that's the case, then every class (resource) you access through the REST api must have a unique identifier (or combination of values that form a unique identifier). – Quark Soup Oct 25 '22 at 20:48
  • 2
    Right - a "combination of values that form a unique identifier", this is what composite keys / IDs are. Adding a surrogate key (your `vechicleId`) is not required to be RESTful. You can still identify the resource with a unique URI, see the several other answers here for valid examples. – Luke Oct 26 '22 at 11:47