2

I'm creating a web API for an application and I have a DELETE action for a resource. The resource is Names, all of which are in a SQL Names table. Each name has the foreign key of a Record (all in a Records table) and each Record has a FileName and IsValid column.

This means that I can delete a bunch of Names by FileName, IsValid, or a combination of these values. This is exactly what the application that will use the API requires.

However, in all the examples of API DELETE endpoints I've seen they are always deleting one record by its id and it's making me doubt if my approach is not considered best practice.

This also brings up a question on how would I even do what I want to do? At the moment, my DELETE endpoint for Names is api/Names/{fileName} and I'm not sure how to also include IsValid into this. At least one or both FileName and IsValid values should be required. I do not want the user to be able to call api/Names and delete every name in the database.

Delete action in NamesController:

[HttpDelete("{fileName}")]
public void DeleteBySourceFileName(string fileName)
{
    _repo.DeleteNamesByFileName(sourceFileName);
}

I've thought about adding IsValid as a query parameter in the action, but that would still keep fileName required.

What would be the best approach to do this, and would such an endpoint be an appropriate for a RESTful API?

Serge
  • 40,935
  • 4
  • 18
  • 45
Lukas
  • 1,699
  • 1
  • 16
  • 49
  • Personally I never delete any records. You never know if you will need them in the future. Just create a field for "deleted" and set it to 0 or 1. (you could use bools, but I prefer tinyints instead of bools) All other logic follows whether that value is set. Also seems like you need the UID unless you can guarantee that the filename is unique. – pcalkins Jan 07 '21 at 19:05
  • If you are deleting a file on the server, maybe consider moving the file into a "deleted" folder instead. Use the UID and table name as part of the filename. – pcalkins Jan 07 '21 at 19:17
  • @pcalkins deleting or not is another issue but here the main issue is how to properly build the URL following the restful style. This is just simple if we consider the endpoint is just a normal web API action, accepting POST request and getting all required data (filename, ...) for the deleting operation. – Hopeless Jan 07 '21 at 19:45
  • Change your action header to this: [Route("~api/DeleteNames/{fileName})] [Route("~api/DeleteIsValid/{IsValid}")] [Route("~api/DeleteBoth/{fileName}/{IsValid}")] public void DeleteBySourceFileName(string fileName, bool? isValid) - all 3 routes should one below another – Serge Jan 07 '21 at 19:48
  • I think deleting operation should accept just one Id and confirmation should be asked. Filtering by arguments like this looks fairly dangerous indeed :D We do filter but usually the results are shown on the UI first for the user to see clearly what should be deleted. There is one exception when we delete all. – Hopeless Jan 07 '21 at 19:48
  • @pcalkins The `fileName` is guaranteed to be unique. Also, even if I have a "Deleted" field, the problem @Hopeless mentions still remains. – Lukas Jan 07 '21 at 19:49
  • @Sergey That's not working because the request matches multiple endpoints which makes sense because if I request `api/Names/{isValid}`, the `isValid` could either be a `string` or a `bool` type since it's just text. – Lukas Jan 07 '21 at 19:57
  • @Lukas using `isValid:bool` in route template may solve a problem in your last comment – Pavel Anikhouski Jan 07 '21 at 20:08
  • @Lukas I didn't offer "api/Names/{isValid}", I offered "~api/DeleteIsValid/{IsValid}"). If you put file name as a parameter you get isValid=null , since only true or false can be valid – Serge Jan 07 '21 at 20:30
  • @Sergey Ahh, I didn't notice. I see now and that makes sense. I know endpoints should preferably contain nouns instead of verbs, but your solution is readable and would work in my case. – Lukas Jan 07 '21 at 20:45

2 Answers2

2

I tested this code in postman. All 3 routs are working properly. Thanks @PavelAnikhouski for isValid:bool idea.

        [Route("~/api/Delete/{fileName}")]
        [Route("~/api/Delete/{isValid:bool}")] 
        [Route("~/api/Delete/{fileName}/{isValid}")] 
        public  void Delete(string fileName, bool? isValid)
        {
            .....
        }

If you like, instead of Delete you can use different names for each route for example "DeleteByFileName" or "DeleteIsValid". It still be working fine.

Serge
  • 40,935
  • 4
  • 18
  • 45
  • Indeed this does work! Thank you. In my code I just had the endpoints say `Names` instead of `Delete` since we're deleting the `Names` resource and the action to `Delete` is specified somewhere in the request. – Lukas Jan 07 '21 at 21:28
2

If you want to follow a RESTful approach, I think you can follow one of three ways to delete several items at once:


  1. DELETE with query string, as already mentioned: DELETE api/names?filenames={file name(s)}&isvalid={true|false}.
    • It is simple, but has the disadvantage of stating the filters as optional, which you already said are not, since you do not allow the deletion of all the Names. The action code would be something like:
[HttpDelete]
public Task Delete([FromQuery] string[] fileNames, [FromQuery] bool? isValid)
{
    // code to delete, but avoid delete with null/empty parameters (avoid delete all)
}

  1. POST to a deletions resource, return an identifier and then DELETE with the identifier, as the workflow below:

---> a) Post request like:

POST api/names/deletions
{
   'filenames': [],
   'isValid': false
}

to an endpoint coded like:

public class DeletionInput
{
    public string[] FileNames { get; set; }
    public bool? IsValid { get; set; }
}

[HttpPost("deletions")]
public Task<IActionResult> CreateDeletion([FromBody] DeletionInput input)
{
    // Based on the inputs, gather a list of Names to be deleted
    // and assign an ID to it. The ID is returned in the response
}

The response of this endpoint returns a location with the deletion ID (e.g. api/names/deletions/3abf5784), so that the client can order the effective delete afterwards.

---> b) Delete request to deletion item created: DELETE api/names/deletions/3abf5784

  • This request deletes the Names list related with the ID 3abf5784. The action would be like:
[HttpDelete("deletions/{id}")]
public Task<IActionResult> DeleteByDeletionId([FromRoute] string id)
{
    // the Names in the deletion list are effectively deleted only now
}

(More info on this option here)


  1. DELETE with body. HTTP delete requests may have body (reference).
  • Request ex.:
DELETE api/names
{
   'filenames': [],
   'isValid': false
}
  • Action ex.:
public class DeletionInput
{
    public string[] FileNames { get; set; }
    public bool? IsValid { get; set; }
}

[HttpDelete]
public Task<IActionResult> Delete([FromBody] DeletionInput input)
{
    // code to delete, but avoid delete with null/empty parameters (avoid delete all)
}
  • It is also a simple option. ASP.NET supports it, but there may be vendors that do not. However, it is allowed by the HTTP specs.
nunohpinheiro
  • 2,169
  • 13
  • 14