59

I started wondering on whether I am not falling into an antipattern here, so please advise on the best practices.

I am designing a REST API with a set of various endpoints and I wanted to wrap the request & response parameters into nice DTO.

For example, a few endpoints:

public async Task<JobStateResponse> GetJobState(JobStateRequest request);
public async Task<JobDownloadRespose> DownloadJob(JobDownloadRequest request);
public async Task<CreateJobResponse> CreateJob(CreateJobRequest request);

The problem is that these requests and responses are relatively similar DTO, for example:

public class JobStateResponse{
    public int TaskId {get;set;}
    public string ExternalId {get;set;}
    public State State {get;set;}
}
public class JobDownloadResponse {
    public int TaskId {get;set;}
    public string ExternalId {get;set;}
    public string JobContent {get;set;}
}

I thought about creating a base class for these and inheriting, but in some cases some of the properties can be redundant... Which means that the methods don't clearly indicate what parameters are needed for them to work OK.

I mean, exposing an API endpoint with a DTO parameter that has 7 properties but only really needs 2 sounds pretty bad...

On the other hand, maintaining separate DTOs for most of the endpoints seems like an overkill as well, and also a maintenance hell.

And also the last thing I want is a complex relationship of several base-base classes for the requests as this may be an even worse maintentance problem.

So, what is the proper approach for request<>response handling?

EDIT: Regarding the 'opinion based' flags - I am looking for best practice for handling this. I know it can be done in multiple ways, but I want to avoid a landmine / antipattern. Also, I gotta say I am pretty happy with the answers so far.

Bartosz
  • 4,406
  • 7
  • 41
  • 80
  • 15
    Thank you for posting this question - its an issue many developers face on a daily basis and is a good candidate to get answers from SO community. – JavaTec Dec 07 '18 at 18:28

3 Answers3

78

Separate, simple DTOs will make your life infinitely easier. It will require more code, but it will be boring, simple code, that is easily tested, and allows your interfaces to change and evolve independently.

Make a DTO for each request endpoint and a separate DTO for each response. Otherwise, you will eventually be sad.

If you find elements that are common to multiple endpoints, extract them into their own object, and include them in both.

And yes, using inheritance here would be wrong. Stick to compositional patterns and you will be fine.

Rob Conklin
  • 8,806
  • 1
  • 19
  • 23
  • Hey... I started wondering - if I go towards composition, then my DTOs will have nested complex types. If I have a complex DTO which in total has several properties, and I send it as HTTP POST, it can be serialized into request body. What if I need to use an HTTP GET? Generic transformation of object to request URI seems pretty complex.. – Bartosz Jun 04 '17 at 19:34
  • 1
    Don't send entities on GETs. GET patterns are almost always retrieving based on identity, or a query. Think about what a GET is from a REST standpoint, it's a request for an object, you don't send the object itself. If you have more the five or six things on your URL, there is probably another object that you are referring to, and you should be using that object's identity. – Rob Conklin Jun 04 '17 at 22:24
  • I have a get endpoint that requires 2 ID values and an enum value to set an option... So, three things, wrapped up in a 'ProjectStateRequest' DTO, to be consistent with a ProjectStateResponse DTO that I return... How would you design that? – Bartosz Jun 05 '17 at 08:22
  • As a POST, you aren't GETting something, you are updating the ProjectState... – Rob Conklin Jun 06 '17 at 16:05
  • the endpoint is GetProjectState, and the reponse returns a project state... – Bartosz Jun 06 '17 at 16:32
  • The endpoint GET should be the query to find the project state, which from your description has two things on it... You shouldn't pass in the project state on the URL, you are trying to retrieve it. I don't think there is a DTO for this, unless you have a ProjectStateQueryDTO... which is a bit excessive. – Rob Conklin Jun 06 '17 at 19:53
  • That is exaclty what I have - I call it a ProjectStateRequest, I pass in two required ID values and return a ProjectStateResponse DTO (which is also very simple). So, are you rather suggesting having these parameters as separate simple types, and only the return value being a ProjectStateResponse DTO? – Bartosz Jun 06 '17 at 20:34
  • This is more judgement-based at this point, but for two parameters I would use simple types, unless those parameters are dependent on each other in some way. So, for instance, if you were looking for ProjectState, and passed a ProjectID and a AsOfDate, then I would say they were not dependent, and could be represented by simple parameters. If it were something more like ProjectGroup and ProjectID, then they would be dependent, and I would consider using a DTO to represent it. But, like I said, this is a judgement call. – Rob Conklin Jun 07 '17 at 01:13
  • Separate DTO will make his life miserable, especially during early development. If dto is a transport mechanism in this case, and if get/post will be straightforward wrapped arround same resource, why bother? Logic of collecting and using data from dto is not in dto itself... – mko Dec 17 '21 at 21:41
  • are you saying a separate request and response dto for each api verb? Post, Put, Patch, Delete, so a total of 8 or something like that? @RobConklin – mattsmith5 Aug 10 '22 at 23:33
  • No, this posting was about different entities (nouns) that had overlapping structures. Post/Put/Patch all have the same noun, and likely use the same DTO, GET/DELETE generally won't have DTOs at all (in most cases) and should generally just use paths. – Rob Conklin Aug 11 '22 at 17:47
12

Using separate DTO enables you to optimise data transfer by the behaviour of the data access calls, it also allows you to restrict access to properties you may not want to expose. Although it is more code, you are defining exactly what is being exposed and are in full control of that.

Mark Redman
  • 24,079
  • 20
  • 92
  • 147
6

You definitely want to separate DTOs per endpoint into 2 gruops: request and response ones.

Now, speaking of inheritance, you could have base Request and base Response class which would include some general, non-specific things (for example, request&response wrapper message into which you plugin your data) but you don't want to mix inheritance between request and response side.

As others already pointed out, it is a bit more code at the begining but that way you'll never hit the wall. On a contrary, mixing them will pretty soon make you have huge refactorings where you'll lose more time&nerves than starting in a clear, separate way.

dee zg
  • 13,793
  • 10
  • 42
  • 82
  • 2
    If a dto is for a same resource/endpoint, use same dto. No point in splitting them between request and response especially for rest api. If you want to hide something, dont map it, set it to null and have your null values not being serialized. Have all you field nullable. Dto is just a mean to transfer data in this case, no need to overcomolicate it. Your true logic should be in domain models or mapping services. – mko Jun 25 '21 at 20:22
  • @mko same endpoint, GET and POST have different DTOs that serve different purpose. – dee zg Dec 09 '21 at 07:28
  • Not necessarily. – mko Dec 17 '21 at 21:36