In this article, you will learn how to create your own API in the Umbraco backoffice. This is a great way to extend the functionality of the Umbraco backoffice and create custom endpoints for your use.
The end result for this article is to create a custom API called "My item API" in the Management API found at /umbraco/swagger/
.
{% hint style="info" %} The Umbraco Backoffice API is also known as the Management API. Thus, a Backoffice API Controller is often referred to as a Management API Controller. {% endhint %}
To create a custom API, you need to create a class that inherits from Umbraco.Cms.Web.BackOffice.Controllers.ManagementApiControllerBase
.
The ManagementApiControllerBase
serves as the foundation for your custom API class. It provides essential functionalities and utilities required for managing APIs within the Umbraco backoffice environment.
We also use the VersionedApiBackOfficeRoute
attribute to define the route for our API. This attribute takes a string parameter that defines the route for the API. This route will be appended to the base route for the backoffice API.
{% code title="MyItemApiController.cs" %}
[VersionedApiBackOfficeRoute("my/item")]
[ApiExplorerSettings(GroupName = "My item API")]
public class MyItemApiController : ManagementApiControllerBase
{
}
{% endcode %}
Now that we have our class set up, we can add an action to get all items. We will use the HttpGet
attribute to define the HTTP
method and route for the action.
The AllItems
field is an in-memory list of items to simulate the use of a repository. We use the skip
& take
parameters here, so users of this endpoint can implement paging. We also use the PagedViewModel
to return the given items (10 by default), and then the total number of items.
{% code title="MyItemApiController.cs" %}
[HttpGet]
public IActionResult GetAllItems(int skip = 0, int take = 10)
=> Ok(
new PagedViewModel<MyItem>
{
Items = AllItems.Skip(skip).Take(take),
Total = AllItems.Count
}
);
{% endcode %}
AllItems is a local list:
{% code title="MyItemApiController.cs" %}
private static readonly List<MyItem> AllItems = Enumerable.Range(1, 100)
.Select(i => new MyItem($"My Item #{i}"))
.ToList();
{% endcode %}
The model for MyItem
is a basic class with an Id
and a Value
property.
{% code title="MyItem.cs" %}
public class MyItem(string value)
{
public Guid Id { get; } = Guid.NewGuid();
public string Value { get; set; } = value;
}
{% endcode %}
We can now create some logic to return a response based on the ID
. The route parameter {id:guid}
specifies that the id
parameter should be a GUID
. Here we're creating a local in-memory list of items and returning the item with the matching ID
.
To note here is the use of the OperationStatusResult
method. This method allows you to return a response with a status code and a body. This is useful for returning error responses with additional information.
The method also needs an enum
operationStatus, as it will be attached to the response. This is a basic example, however, this OperationStatus
would be returned from your service layer, based on the error in the service layer method.
{% code title="MyItemApiController.cs" %}
[HttpGet("{id:guid}")]
public IActionResult GetItem(Guid id)
{
MyItem? item = AllItems.FirstOrDefault(item => item.Id == id);
return item is not null
? Ok(item)
: OperationStatusResult(
MyItemOperationStatus.NotFound,
builder => NotFound(
builder
.WithTitle("That thing wasn't there")
.WithDetail("Maybe look for something else?")
.Build()
)
);
}
{% endcode %}
{% code title="MyItemOperationStatus.cs" %}
public enum MyItemOperationStatus
{
NotFound,
InvalidValue,
DuplicateValue
}
{% endcode %}
We can now add an action to create a new item. We use the HttpPost
attribute to define the HTTP
method and route for the action. Here we can see some validation logic.
If the value does not start with "New", we return a BadRequest
response with an error message. This highlights why we use the OperationStatusResult
method. We can return a detailed response.
We also use CreatedAtId<MyItemApiController>
, a helper method to create a response with a 201 Created
status code and a Location
header.
{% code title="MyItemApiController.cs" %}
[HttpPost]
public IActionResult CreateItem(string value)
{
if (value.StartsWith("New") is false)
{
return OperationStatusResult(
MyItemOperationStatus.InvalidValue,
builder => BadRequest(
builder
.WithTitle("That was invalid")
.Build()
)
);
}
if (AllItems.Any(item => item.Value.InvariantEquals(value)))
{
return OperationStatusResult(
MyItemOperationStatus.DuplicateValue,
builder => BadRequest(
builder
.WithTitle("No duplicate values, please.")
.Build()
)
);
}
var newItem = new MyItem(value);
AllItems.Add(newItem);
return CreatedAtId<MyItemApiController>(
ctrl => nameof(ctrl.GetItem),
newItem.Id
);
}
{% endcode %}
Now we can add an action to update an item. We can use the HttpPut
attribute to define the HTTP
method and route for the action.
{% code title="MyItemApiController.cs" %}
[HttpPut("{id:guid}")]
public IActionResult UpdateItem(Guid id, string value)
{
MyItem? item = AllItems.FirstOrDefault(item => item.Id == id);
if (item is null)
{
return OperationStatusResult(
MyItemOperationStatus.NotFound,
builder => NotFound(
builder
.WithTitle("That thing wasn't there")
.WithDetail("Maybe look for something else?")
.Build()
)
);
}
if (AllItems.Any(i => i.Value.InvariantEquals(value)))
{
return OperationStatusResult(
MyItemOperationStatus.DuplicateValue,
builder => BadRequest(
builder
.WithTitle("You have been DUPED")
.Build()
)
);
}
item.Value = value;
return Ok();
}
{% endcode %}
Finally, we can add an action to delete an item. We can use the HttpDelete
attribute to define the HTTP
method and route for the action.
{% code title="MyItemApiController.cs" %}
[HttpDelete("{id:guid}")]
public IActionResult DeleteItem(Guid id)
{
MyItem? item = AllItems.FirstOrDefault(item => item.Id == id);
if (item is null)
{
return OperationStatusResult(
MyItemOperationStatus.NotFound,
builder => NotFound(
builder
.WithTitle("That thing wasn't there")
.WithDetail("Maybe look for something else?")
.Build()
)
);
}
AllItems.Remove(item);
return Ok();
}
{% endcode %}
Now we have created the custom API for our Umbraco project. Below you can see the full example of the implementation.
{% code title="MyItemApiController.cs" %}
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Controllers;
using Umbraco.Cms.Api.Management.Routing;
namespace UmbracoDocs.Samples;
[VersionedApiBackOfficeRoute("my/item")]
[ApiExplorerSettings(GroupName = "My item API")]
public class MyItemApiController : ManagementApiControllerBase
{
private static readonly List<MyItem> AllItems = Enumerable.Range(1, 100)
.Select(i => new MyItem($"My Item #{i}"))
.ToList();
[HttpGet]
public IActionResult GetAllItems(int skip = 0, int take = 10)
=> Ok(
new PagedViewModel<MyItem>
{
Items = AllItems.Skip(skip).Take(take),
Total = AllItems.Count
}
);
[HttpGet("{id:guid}")]
public IActionResult GetItem(Guid id)
{
MyItem? item = AllItems.FirstOrDefault(item => item.Id == id);
return item is not null
? Ok(item)
: OperationStatusResult(
MyItemOperationStatus.NotFound,
builder => NotFound(
builder
.WithTitle("That thing wasn't there")
.WithDetail("Maybe look for something else?")
.Build()
)
);
}
[HttpPost]
public IActionResult CreateItem(string value)
{
if (value.StartsWith("New") is false)
{
return OperationStatusResult(
MyItemOperationStatus.InvalidValue,
builder => BadRequest(
builder
.WithTitle("That was invalid")
.Build()
)
);
}
if (AllItems.Any(item => item.Value.InvariantEquals(value)))
{
return OperationStatusResult(
MyItemOperationStatus.DuplicateValue,
builder => BadRequest(
builder
.WithTitle("No duplicate values, please.")
.Build()
)
);
}
var newItem = new MyItem(value);
AllItems.Add(newItem);
return CreatedAtId<MyItemApiController>(
ctrl => nameof(ctrl.GetItem),
newItem.Id
);
}
[HttpPut("{id:guid}")]
public IActionResult UpdateItem(Guid id, string value)
{
MyItem? item = AllItems.FirstOrDefault(item => item.Id == id);
if (item is null)
{
return OperationStatusResult(
MyItemOperationStatus.NotFound,
builder => NotFound(
builder
.WithTitle("That thing wasn't there")
.WithDetail("Maybe look for something else?")
.Build()
)
);
}
if (AllItems.Any(i => i.Value.InvariantEquals(value)))
{
return OperationStatusResult(
MyItemOperationStatus.DuplicateValue,
builder => BadRequest(
builder
.WithTitle("You have been DUPED")
.Build()
)
);
}
item.Value = value;
return Ok();
}
[HttpDelete("{id:guid}")]
public IActionResult DeleteItem(Guid id)
{
MyItem? item = AllItems.FirstOrDefault(item => item.Id == id);
if (item is null)
{
return OperationStatusResult(
MyItemOperationStatus.NotFound,
builder => NotFound(
builder
.WithTitle("That thing wasn't there")
.WithDetail("Maybe look for something else?")
.Build()
)
);
}
AllItems.Remove(item);
return Ok();
}
}
public class MyItem(string value)
{
public Guid Id { get; } = Guid.NewGuid();
public string Value { get; set; } = value;
}
public enum MyItemOperationStatus
{
NotFound,
InvalidValue,
DuplicateValue
}
{% endcode %}