Skip to Content

Models and DTOs in Your API

Why you should avoid using model objects in your API interfaces

I religiously avoid using model objects on my API interfaces. The reason is really simple, a model is a business entity and you don’t want to expose a business entity inadvertently to your API consumers.

What’s the problem ?

Consider the following model being used by the business layer of your application and also being returned in a get method of your API

1
2
3
4
5
6
7
public class Product
{
	public int Id { get; set; }
	public string Sku { get; set; }
	public string Name { get; set; }
	public decimal ListPrice { get; set; }
}

There’s nothing special here but now the business wants to track the product markup and you decided to go with a new property:

1
2
3
4
5
6
7
8
public class Product
{
	public int Id { get; set; }
	public string Sku { get; set; }
	public string Name { get; set; }
	public decimal ListPrice { get; set; }
	public decimal MarkUp { get; set; }
}

Now your API is also sending the product markup. Does you business want to let everybody know they are marking-up a product by 3000% ? Maybe or maybe not. Maybe your APIs are consumed only by internal apps and they are aware and have the responsability of dealing with this information but on the other hand you might be exposing the same API to external access.

How to determine when to use a DTO or a model in you API

As I said previously you might be exposing your API only to internal apps which by sending the same Product model would be harmless. But you might be exposing the same API to external access. So the answer is a straight “it depends”. Since I don’t want to have a different approach for every single case and I like uniformity throughout my app I always go with DTOs even though they might be exactly the same as the model. The DTO allows your team-members and business to make a concious decision whether they want to share a piece of information to whoever is pulling this data from the API.

But be careful when using AutoMapper ⚠️

I use AutoMapper to map my models into DTOs and vice-versa. Let’s consider the following Product DTO

1
2
3
4
5
6
7
public class ProductDto
{
	public int Id { get; set; }
	public string Sku { get; set; }
	public string Name { get; set; }
	public decimal ListPrice { get; set; }
}

and the following Product which has already been added

1
2
3
4
5
6
{
    "id": 1,
    "sku": "abc01",
    "name": "useless stuff",
    "listPrice": 10.99
}

I want to change the name from “useless stuff” to “super useless stuff”. My Put API is implemented as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Route("{ProductId:int}")]
[HttpPut]
public async Task<IActionResult> Put(int productId, [FromBody] ProductDto dto)
{
	try
	{
		if (!ModelState.IsValid) return BadRequest(ModelState);
		var oldProduct = await _productRepo.GetAsync(productId);
		if (oldProduct == null) return NotFound();
		_mapper.Map(dto, oldProduct);
		await _productRepo.UpdateAsync(oldProduct);
		await _productRepo.SaveAllAsync();

		return Ok(_mapper.Map<Product, ProductDto>(oldProduct));
	}
	catch (Exception e)
	{
		_log.LogError($"error updating product {e}");
	}

	return BadRequest("Error updating product");
}

We are receiving a ProductDto (line 3), pulling the product from the database, mapping the dto with it (line 10) and returning a converted ProductDto to the caller (line 14)

Request:

1
2
3
4
5
6
//PUT http://localhost:59122/api/products/1
{
	"sku":"abc01",
	"name":"super useless stuff",
	"listPrice":"10.99"
}

Response:

1
2
3
4
5
6
{
    "id": 0,
    "sku": "abc01",
    "name": "super useless stuff",
    "listPrice": 10.99
}

I wanted to update product id 1 but why am I seeing id 0 ?

You didn’t pass any value for the Id property so the default value was set to 0. AutoMapper identified this property in the Dto and added it to the Model pulled from the Database. In the end the new merged model was updated to 0.

How to guarantee I’m not updating what I don’t want to update ?

You could check whether the DTO Id is > 0 and receive the following request:

1
2
3
4
5
6
7
//PUT http://localhost:59122/api/products/1
{
	"id": 1,
	"sku":"abc01",
	"name":"super useless stuff",
	"listPrice":"10.99"
}

The request contains the same id in both the url (line 1) and the body (line 3)

You would also need to make sure the id from the DTO (line 3) is the same as the id passed in the url otherwise the following request would update the wrong product:

1
2
3
4
5
6
7
//PUT http://localhost:59122/api/products/1
{
	"id": 5,
	"sku":"abc01",
	"name":"super useless stuff",
	"listPrice":"10.99"
}

That’s a lot of manual checks for your controller to do. What if we guarantee they cannot update the id by simply not receiving it in the DTO ? Let’s remove the id property from the DTO:

1
2
3
4
5
6
public class ProductDto
{
	public string Sku { get; set; }
	public string Name { get; set; }
	public decimal ListPrice { get; set; }
}

and make a new request:

1
2
3
4
5
6
//PUT http://localhost:59122/api/products/1
{
	"sku":"abc01",
	"name":"super useless stuff",
	"listPrice":"10.99"
}

response:

1
2
3
4
5
{
    "sku": "abc01",
    "name": "super useless stuff",
    "listPrice": 10.99
}

Did it work ?

I don’t know. I want to be able to see the Id in the response.

1 DTO for the request and another one for the response.

We want to have a DTO that doesn’t receive the Id property for the request but we do want to send everything + the Id value as a response. Hello ProductDto and ProductResponseDto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ProductDto
{
	public string Sku { get; set; }
	public string Name { get; set; }
	public decimal ListPrice { get; set; }
}

public class ProductResponseDto:ProductDto
{
	public int Id { get; set; }
}

New Put method

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
        [Route("{ProductId:int}")]
        [HttpPut]
        public async Task<IActionResult> Put(int productId, [FromBody] ProductDto dto)
        {
            try
            {
                if (!ModelState.IsValid) return BadRequest(ModelState);
                var oldProduct = await _productRepo.GetAsync(productId);
                if (oldProduct == null) return NotFound();
                _mapper.Map(dto, oldProduct);
                await _productRepo.UpdateAsync(oldProduct);
                await _productRepo.SaveAllAsync();

                return Ok(_mapper.Map<Product, ProductResponseDto>(oldProduct));
            }
            catch (Exception e)
            {
                _log.LogError($"error updating product {e}");
            }

            return BadRequest("Error updating product");
        }
We are receiving a ProductDto (line 3) and returning a ProductResponseDto (line 14)

request:

1
2
3
4
5
6
//PUT http://localhost:59122/api/products/1
{
	"sku":"abc01",
	"name":"super useless stuff",
	"listPrice":"10.99"
}

response:

1
2
3
4
5
6
{
    "id": 1,
    "sku": "abc01",
    "name": "super useless stuff",
    "listPrice": 10.99
}

Yes, It is working 👍 and since we are not receiving an Id in the dto we can make a request like the following:

1
2
3
4
5
6
7
//PUT http://localhost:59122/api/products/1
{
	"id": 5,
	"sku":"abc01",
	"name":"super useless stuff",
	"listPrice":"20.99"
}

and the id will be safelly ignored.

response:

1
2
3
4
5
6
{
    "id": 1,
    "sku": "abc01",
    "name": "super useless stuff",
    "listPrice": 20.99
}

Happy coding!

comments powered by Disqus