The goal of this project is to document the iterative process of implementing and scaling an API by first building a monolithic structure, and then refactoring to distributed microservices.
Since this project is focused on implementing an API, rather than designing one, we will be using the Spotify Public API as a reference for our functional requirements and data model.
An application that is designed to run within a single process, on a single server, with a single database schema.
- Clean Architecture (aka Onion Architecture, Ports & Adapters, Hexagonal Architecture)
- ASP.NET Core Minimal API
- Supplemented by Carter
- MongoDB
- Mediatr
blahblhablh
Let's jump right in and start playing with the Spotify Developer Console to get a feel for the types of queries and commands we can execute, and the data models that are returned.
Try out the Get Artist console:
{
"external_urls": {
"spotify": "https://open.spotify.com/artist/5wFXmYsg3KFJ8BDsQudJ4f"
},
"followers": {
"href": null,
"total": 427177
},
"genres": [
"indie rock"
],
"href": "https://api.spotify.com/v1/artists/5wFXmYsg3KFJ8BDsQudJ4f",
"id": "5wFXmYsg3KFJ8BDsQudJ4f",
"images": [
{
"height": 640,
"url": "https://i.scdn.co/image/ab6761610000e5eb642fbb74e3e7507c12d8b8fd",
"width": 640
},
{
"height": 320,
"url": "https://i.scdn.co/image/ab67616100005174642fbb74e3e7507c12d8b8fd",
"width": 320
},
{
"height": 160,
"url": "https://i.scdn.co/image/ab6761610000f178642fbb74e3e7507c12d8b8fd",
"width": 160
}
],
"name": "Manchester Orchestra",
"popularity": 64,
"type": "artist",
"uri": "spotify:artist:5wFXmYsg3KFJ8BDsQudJ4f"
}
Look carefully among the sea of URLs and you'll find the information we care about right now:
- id
- name
- genres
We'll come back for the other properties later on, but this is a good starting point for our Artist
entity:
namespace Dotify.Core.Artists.Entities;
public interface IArtist
{
string Id { get; set; }
string Name { get; set; }
List<string> Genres { get; set; }
}
We'll be organizing our API into Features, so let's go ahead and create our Artist
feature and build out its API module.
First, we need to define an interface that abstracts the act of 'Querying For All Artists
'. We do this in our Core project, so that we can implement it any way we need to. For instance, we might mockup an in-memory List for testing, or switch from MongoDB to CosmosDB, and nothing has to change.
using Dotify.Core.Artists.Entities;
namespace Dotify.Core.Artists.Queries;
public interface IGetArtistsQuery
{
IEnumerable<Artist> Execute();
}
Now, in our API project we can implement this interface using our MongoDB Artist
collection:
using Dotify.Api.Features.Artists.Data;
using Dotify.Core.Artists.Entities;
using Dotify.Core.Artists.Queries;
using MongoDB.Driver;
namespace Dotify.Api.Features.Artists.Queries;
public class GetArtistsQuery : IGetArtistsQuery<ArtistDto>
{
private readonly ArtistCollection _collection;
public GetArtistsQuery(ArtistCollection collection)
{
_collection = collection;
}
public async Task<IEnumerable<ArtistDto>> ExecuteAsync()
{
var filter = Builders<Artist>.Filter.Empty;
var results = await _collection.Artists.FindAsync(filter);
var artists = await results.ToListAsync();
var artistDtos = new List<ArtistDto>(artists.Count);
artists.ForEach(a => artistDtos.Add(new ArtistDto(a)));
return artistDtos;
}
}
Wire up our query into the MSDI IServiceCollection
:
services.AddSingleton<IGetArtistsQuery<ArtistDto>, GetArtistsQuery>();
Finally, let's create the API endpoint. We use Carter to simplify and enhance ASP.NET Core Minimal API -- it provides some handy functionality and extension methods for us to use.
using Dotify.Api.Features.Artists.Data;
using Dotify.Core.Artists.Queries;
namespace Dotify.Api.Features.Artists.Modules;
public class ArtistModule : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/artists", async (IGetArtistsQuery<ArtistDto> getArtistsQuery, HttpResponse res) =>
{
var artists = await getArtistsQuery.ExecuteAsync();
return artists;
})
.IncludeInOpenApi();
}
}
Analyzing the Spotify data model, we find many common fields which are shared across the majority of responses.
For example, a response for a query on a specific artist:
{
"id": "5wFXmYsg3KFJ8BDsQudJ4f",
"href": "https://api.spotify.com/v1/artists/5wFXmYsg3KFJ8BDsQudJ4f",
"uri": "spotify:artist:5wFXmYsg3KFJ8BDsQudJ4f",
"external_urls": {
"spotify": "https://open.spotify.com/artist/5wFXmYsg3KFJ8BDsQudJ4f"
},
"type": "artist",
...
}
Contains various ways to link to the artist record:
- A unique ID
- A reference to the API endpoint where information on the artist can be found
- A URI which, presumably, is used for navigation within Spotify's applications
- An external URL which points to the Spotify Web Player
We'll ultimately utilize a better solution, but for now tack this on our Data Transfer Object (DTO) implementation of our IArtist
entity, by implementing the ILocatableEntity
interface:
using Dotify.Core.Artists.Entities;
using Dotify.Core.Shared;
namespace Dotify.Api.Features.Artists.Data;
public class ArtistDto : IArtist, ILocatableEntity
{
public string Id { get; set; }
public List<string> Genres { get; set; }
public string Name { get; set; }
public string Href { get; }
public string Uri { get; }
public string Type { get; }
public ArtistDto(IArtist a)
{
Id = a.Id;
Name = a.Name;
Genres = a.Genres;
Type = a.GetType().Name.ToLower();
Href = $"https://localhost:7122/artists/{a.Id}";
Uri = $"dotify:artists:{a.Id}";
}
}