Share this post
  

Create a React-based app with ASP.NET Minimal APIs and MongoDB storage

I’ve been using React for while by mixing it up with .NET stuff and enjoying it a lot. For this post, I decided to combine them along with Docker Compose, ASP.NET Minimal APIs and MongoDB support!

Using React

I use React the tech behind my blog (mostly NodeJS/Jamstack client-side generated which I migrated recently from existing code that has been around ~2015) both for the client and server side rendering.

So this time I decided to set a sample to combine React and .NET in a sample app and ended-up packing it as Pull-Request #248 for the Awesome-Compose repo from Docker in GitHub, following same structure I did for previous .NET contributions into that repo.

If you are interested in previous posts about Awesome-Compose, either curious about Docker-izing .NET 5/6 apps or using GitHub Codespaces for development you can browse them from here and here.

The code for this post is also available from my GitHub under this repo:

https://github.com/stvansolano/codebox-react-dotnet-mongodb

So, let’s get started! A few things you may need to install:

  • Docker Compose
  • .NET 6 SDK
  • NodeJS

Side-note: I didn’t really have to install anything while developing this on my local as I was using GitHub Codespaces with my some tailored devcontainer setup, so I can focus on development!

Preview

1) Creating the /frontend: A simple TODO app.

For this sample, we will use react-scripts scaffold for creating a basic app and use a very simple, still classic sample for having a TODO app in order to focus on JavaScript, NodeJS/Npx stuff. For this sample, I also re-used pretty much of the React sample from Awesome-Compose repo, which is fairly common and straightforward:

Frontend structure

In order to consume our API later from ASP.NET, let’s add a NGINX file for taking care of the API calls later from our ASP.NET Web API. Things will run on their own NGINX-based container so the backend is de-coupled from the frontend.

server {
    listen       80;
    server_name  localhost;
    
    server_tokens off;
    proxy_hide_header X-Powered-By;
    proxy_hide_header Server;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload';
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-Permitted-Cross-Domain-Policies master-only;
    add_header Referrer-Policy same-origin;
    add_header Expect-CT 'max-age=60';
    
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    location /api {
        proxy_set_header  Host $host;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Host $server_name;
        proxy_pass        http://backend:8000;
        proxy_redirect    default;
    }
}

Important: The .NET CLI and also Visual Studio provide a great set of pre-defined templates that also include React, Angular and more great stuff, which I encourage to give it a try. Also, pretty much of this tutorial can be also take .NET on the frontend, with the help of amazing Blazor 💖✨

2) Now it’s time for .NET in the /backend: Set up Minimal API for MongoDB

Here things become more interesting. By scaffolding a new Web API from .NET 6, we can implement a very-simple, still small program to call MongoDB collection and support some basic operations for our API, and we can add Swagger+OpenAPI support with a few lines of code:

using System;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Models;
using Swashbuckle.AspNetCore.SwaggerGen;

var builder = WebApplication.CreateBuilder(args);

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

string connectionString = builder.Configuration.GetConnectionString("DocumentDbConnection");
string databaseName = builder.Configuration.GetConnectionString("DocumentDbName") ?? "BackendMongoDb";
string collectionName = builder.Configuration.GetConnectionString("DocumentCollectionName") ?? "ToDos";

builder.Services.AddTransient<MongoClient>((_provider) => new MongoClient(connectionString));

var app = builder.Build();

var isSwaggerEnabledFromConfig = bool.TrueString.Equals(builder.Configuration["EnableSwagger"] ?? "", StringComparison.OrdinalIgnoreCase);
if (isSwaggerEnabledFromConfig) 
{
    Console.WriteLine("Swagger enabled via appsettings.json");
}

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || isSwaggerEnabledFromConfig)
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapGet("/api/todos", async (MongoClient connection) =>
{
    try
    {
        var database = connection.GetDatabase(databaseName);
        var collection = database.GetCollection<ToDo>(collectionName);
        var results = await collection.Find(_ => true).ToListAsync().ConfigureAwait(false);

        return Results.Ok(results);
    }
    catch (Exception ex)
    {
        return Results.Problem(detail: ex.ToString());
    }
});

app.MapGet("/api/todos/{id}", async (string id, MongoClient connection) =>
{
    try
    {
        var database = connection.GetDatabase(databaseName);
        var collection = database.GetCollection<ToDo>(collectionName);
        var result = await collection.FindAsync(record => record.Id == id).ConfigureAwait(false) as ToDo;
        
        if (result is null) 
        {
            return Results.NotFound();
        }

        return Results.Created($"/todoitems/{result.Id}", result);
    }
    catch (Exception ex)
    {
        return Results.Problem(detail: ex.ToString());
    }
});

app.MapPost("/api/todos", async (ToDo record, MongoClient connection) =>
{
    try
    {
        var database = connection.GetDatabase(databaseName);
        var collection = database.GetCollection<ToDo>(collectionName);
        await collection.InsertOneAsync(record).ConfigureAwait(false);

        return Results.Created($"/api/todos/{record.Id}", record);
    }
    catch (Exception ex)
    {
        return Results.Problem(detail: ex.ToString());
    }
});

app.Run();

Here is a screenshot of the backend when opening the /swagger endpoint:

Backend swagger

3) Oh! Yes, MongoDB. Let’s Compose it and wrap up with .NET!

Last but not least, let’s have the Frontend in place, connect it to the Backend and store our To-Dos in MongoDB.

We can use Compose services here by just grabbing the container images and set things up.

 services:
  frontend:
    build:
      context: frontend
    ports:
      - 80:80
    volumes:
      - '.:/app'
      - '/app/node_modules'
    networks:
      - react-frontend
    depends_on:
      - backend
    links:
      - backend

  backend:
    build: backend
    restart: always
    ports:
      - 8000:8000
    depends_on: 
      - mongo
    links:
      - mongo
    environment:
      - ASPNETCORE_URLS=http://+:8000
      - EnableSwagger=true
    networks:
      - react-backend
      - react-frontend

  mongo:
    restart: always
    image: mongo:4.2.0
    volumes:
      - ./data:/data/db
    networks:
      - react-backend

  mongo-express:
    image: mongo-express
    restart: always
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: example
    depends_on: 
      - mongo
    links:
      - mongo
    networks:
      - react-backend

networks:
  react-backend: {}
  react-frontend: {} 

The compose file also includes a Mongo-Express server in the Compose file so we can quickly explore the NoSQL documents and check everything end-to-end from UI in React either Swagger document generated for us with .NET

Also, I added a EnableSwagger flag as a Environment variable, which can be read and set accordingly in case you wan to expose Swagger endpoint. To learn more about Appsettings.json, environment variables and overriding them for Docker containers you can follow this link:

Bonus tip: CosmosDB support out of the box!

We can take advantage of using Azure CosmosDB, which supports MongoDB connector so it’s pretty easy to set-up and swap from vanilla MongoDB to use CosmosDB. Here I include some links for reference along with more samples to follow-up:

Happy coding!

Share this post
  


@stvansolano
More about me