Creating Generic RESTful API Services
I have recently been designing the RESTful API’s for a new application I’m involved in building. There are likely to be dozens or even hundreds of API’s required by the application once it is fully complete. My goal when thinking about designing these RESTfulAPI’s was how to implement them in such a way as to reduce the exposed surface area to the client, such that fewer RESTful API’s would be required to fulfill all service requests.
My thought process got me wondering if a single RESTful endpoint would be sufficient to handle all CRUD operations. This would need to handle multiple data types such as users, quotes, vehicles, purchase orders etc (the application is aimed at the fleet management sector) for the application. Usually a single endpoint would be created for each of the different data types i.e. a single RESTful endpoint for handling all driver CRUD operations, a single RESTful endpoint for handling all vehicle CRUD operations.
As I stated previously though, I wanted to design the RESTful API’s in such a way as to reduce the exposed surface area and therefore try to perform all these CRUD operations using a single RESTful API.
After some trial and error, I got this working using what turned out to be a simple design pattern. I’ll explain the design pattern for the GET (read) operations, and leave the others as an exercise for the reader to work out.
For each GET operation I pass two parameters. The first parameter identifies the type of query that is required and is a unique string identifier. It can hold values such as “getuserbyemail”, “getuserpermissions”, “getallusers”. The second parameter is a JSONarray structure containing key-value pairs of the values needed to fulfill the GET operation. As such it can contain a user’s email address, a user’s ID, a vehicle registration and so on.
Example JSON array structure.
Hide Copy Code
{"QuerySearchTerms":{"email":"test@mycompany.co.uk"}}
The code for the GET request receives these two parameters on the querystring. After some initial validation checks (such as ensuring the request is authorised, time-bound and that both parameters are valid), it then processes the request.
The first querystring parameter informs the RESTful API what type of request is being made, and so therefore what elements to extract from the JSON array (which is the second querystring parameter). Here is the array structure that is passed to the GETrequest implemented in C#. This array structure can easily be (de)serialised and passed as a string parameter to the request.
Hide Copy Code
[DataContract]
public class WebQueryTasks
{
[DataMember]
public Dictionary<string, object> QuerySearchTerms { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public WebQueryTasks()
{
this.QuerySearchTerms = new Dictionary<string, object>();
}
}
Here is the skeleton code for the GET request. For clarity I have removed the logging, the validation checks and kept the code as simple as possible.
Hide Expand
Copy Code
public string WebGetData(string queryname, string queryterms)
{
//initial checks such as validation and parameter checking
try
{
//deserialise the JSON array structure
WebQueryTasks query = ManagerHelper.SerializerManager().DeserializeObject<WebQueryTasks>(queryterms);
if (query == null || !query.QuerySearchTerms.Any())
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest, new HttpError("Unable to deserialise search terms.")));
}
object temp;
string webResults;
//detemine the type of the query
switch (queryname.ToLower())
{
case WebTasksTypeConstants.GetCompanyByName:
webResults = this._userService.GetQuerySearchTerm("name", query);
if (!string.IsNullOrEmpty(webResults))
{
temp = this._companiesService.Find(webResults);
}
else
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest, new HttpError("Unable to locate query search term(s).")));
}
break;
case WebTasksTypeConstants.GetUserByEmail:
webResults = this._userService.GetQuerySearchTerm("email", query);
if (!string.IsNullOrEmpty(webResults))
{
temp = this._userService.FindByEmail(webResults);
}
else
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest, new HttpError("Unable to locate query search term(s).")));
}
break;
default:
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest,
new HttpError($"Unknown query type {queryname}.")));
}
var result = ManagerHelper.SerializerManager().SerializeObject(temp);
return result;
}
catch (Exception ex)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest, new HttpError("Exception servicing request.")));
}
}
I have applied the same design pattern to all the requests (POST, PUT, GET and DELETE). I pass in the same two parameters on the querystring, and the RESTful API determines what needs to be processed, and fetches the relevant values from the JSON array to process it. All data is returned in JSON format.
I have found this design pattern to be extremely flexible, extensible and easy to work with. It allows for all / any type of request to be made in a very simple manner. I have impemented full CRUD operations on a number of different data types all without a problem using this design pattern.