Build & Containerize Product App with React JS .NET Core Docker

Introduction

In this article, we will create a sample product application backend using the .NET Core Web API and web forms using React JS. Also, we will containerize the same with the help of docker.

Agenda

  • Sample Product Application Backend (.NET Core Web API)
  • Sample Product Application Frontend (React JS)
  • Docker Files for Application
  • Containerize the Application

Prerequisites

  • Visual Studio 2022
  • Docker Desktop
  • NPM
  • .NET Core SDK
  • React JS

Sample Product Application: Backend (.NET Core Web API)

Step 1. Create a new Product Management .NET Core Web API.

Step 2. Please install the following NuGet packages we used for database migrations and connectivity with SQL Server.

 NuGet packages

Step 3. Add the product class inside the entities folder.

namespace ProductManagementAPI.Entities
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

Step 4. Create an AppDbContext class inside the data folder with a SQL Server connection and a DB set property.

using Microsoft.EntityFrameworkCore;
using ProductManagementAPI.Entities;
namespace ProductManagementAPI.Data
{
    public class AppDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }

        protected readonly IConfiguration Configuration;

        public AppDbContext(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
        }
    }
}

Step 5. Add a product repository inside the repositories folder.

IProductRepository

using ProductManagementAPI.Entities;
namespace ProductManagementAPI.Repositories
{
    public interface IProductRepository
    {
        void AddProduct(Product product);
        void DeleteProduct(int id);
        List<Product> GetAllProducts();
        Product GetProductById(int id);
        void UpdateProduct(Product product);
    }
}

ProductRepository

using Microsoft.EntityFrameworkCore;
using ProductManagementAPI.Data;
using ProductManagementAPI.Entities;
namespace ProductManagementAPI.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly AppDbContext _context;

        public ProductRepository(AppDbContext context)
        {
            _context = context;
        }

        public List<Product> GetAllProducts()
        {
            return _context.Products.ToList();
        }

        public Product GetProductById(int id)
        {
            return _context.Products.FirstOrDefault(p => p.Id == id);
        }

        public void AddProduct(Product product)
        {
            if (product == null)
            {
                throw new ArgumentNullException(nameof(product));
            }

            _context.Products.Add(product);
            _context.SaveChanges();
        }

        public void UpdateProduct(Product product)
        {
            if (product == null)
            {
                throw new ArgumentNullException(nameof(product));
            }

            _context.Entry(product).State = EntityState.Modified;
            _context.SaveChanges();
        }

        public void DeleteProduct(int id)
        {
            var product = _context.Products.Find(id);
            if (product == null)
            {
                throw new ArgumentNullException(nameof(product));
            }

            _context.Products.Remove(product);
            _context.SaveChanges();
        }
    }
}

Step 6. Create a new product controller with different action methods that we used to perform different operations using our front-end application after invoking the same.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ProductManagementAPI.Entities;
using ProductManagementAPI.Repositories;
namespace ProductManagementAPI.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductController : ControllerBase
    {
        private readonly IProductRepository _productRepository;

        public ProductController(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }

        [HttpGet]
        public IActionResult GetAllProducts()
        {
            var products = _productRepository.GetAllProducts();
            return Ok(products);
        }

        [HttpGet("{id}")]
        public IActionResult GetProductById(int id)
        {
            var product = _productRepository.GetProductById(id);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }

        [HttpPost]
        public IActionResult AddProduct([FromBody] Product product)
        {
            if (product == null)
            {
                return BadRequest();
            }

            _productRepository.AddProduct(product);
            return CreatedAtAction(nameof(GetProductById), new { id = product.Id }, product);
        }

        [HttpPut("{id}")]
        public IActionResult UpdateProduct(int id, [FromBody] Product product)
        {
            if (product == null || id != product.Id)
            {
                return BadRequest();
            }

            var existingProduct = _productRepository.GetProductById(id);
            if (existingProduct == null)
            {
                return NotFound();
            }

            _productRepository.UpdateProduct(product);
            return NoContent();
        }

        [HttpDelete("{id}")]
        public IActionResult DeleteProduct(int id)
        {
            var existingProduct = _productRepository.GetProductById(id);
            if (existingProduct == null)
            {
                return NotFound();
            }

            _productRepository.DeleteProduct(id);
            return NoContent();
        }
    }
}

Step 7. Open the app settings file and add the database connection string.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=DESKTOP-8RL8JOG;Initial Catalog=ReactNetCoreCrudDb;User Id=sa;Password=database@1;"
  }
}

Step 8. Register our services inside the service container and configure the middleware.

using ProductManagementAPI.Data;
using ProductManagementAPI.Repositories;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddCors(options => {
    options.AddPolicy("CORSPolicy", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseCors("CORSPolicy");
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Step 9. Execute the following entity framework database migration command to create a database and tables.

add-migration "v1"
update-database

Step 10. Finally, run the application and use Swagger UI to execute different API endpoints.

Swagger UI

Sample Product Application: Frontend (React JS)

Let’s create a client application using React JS and consume the above API endpoints within it.

Step 1. Create a new React JS application with the help of the following command.

npx create-react-app react-netcore-crud-app

Step 2. Navigate to your project directory.

cd react-netcore-crud-app

Step 3. Install Axios to consume and hit backend API and bootstrap for designing purposes.

npm install axios
npm install bootstrap

Step 4. Add the following components and services.

Product list component

// src/components/ProductList/ProductList.js
import React, { useState, useEffect } from 'react';
import ProductListItem from './ProductListItem';
import productService from '../../services/productService';
const ProductList = () => {
    const [products, setProducts] = useState([]);
    useEffect(() => {
        fetchProducts();
    }, []);
    const fetchProducts = async () => {
        try {
            const productsData = await productService.getAllProducts();
            setProducts(productsData);
        } catch (error) {
            console.error('Error fetching products:', error);
        }
    };
    const handleDelete = async (id) => {
        try {
            await productService.deleteProduct(id);
            fetchProducts(); // Refresh product list
        } catch (error) {
            console.error('Error deleting product:', error);
        }
    };
    const handleEdit = () => {
        fetchProducts(); // Refresh product list after editing
    };
    return (
        <div className="container">
            <h2 className="my-4">Product List</h2>
            <ul className="list-group">
                {products.map(product => (
                    <ProductListItem key={product.id} product={product} onDelete={() => handleDelete(product.id)} onEdit={handleEdit} />
                ))}
            </ul>
        </div>
    );
};
export default ProductList;

Product list item component

// src/components/ProductList/ProductListItem.js
import React, { useState } from 'react';
import productService from '../../services/productService';
const ProductListItem = ({ product, onDelete, onEdit }) => {
    const [isEditing, setIsEditing] = useState(false);
    const [editedName, setEditedName] = useState(product.name);
    const [editedPrice, setEditedPrice] = useState(product.price);
    const handleEdit = async () => {
        setIsEditing(true);
    };
    const handleSave = async () => {
        const editedProduct = { ...product, name: editedName, price: parseFloat(editedPrice) };
        try {
            await productService.updateProduct(product.id, editedProduct);
            setIsEditing(false);
            onEdit(); // Refresh product list
        } catch (error) {
            console.error('Error updating product:', error);
        }
    };
    const handleCancel = () => {
        setIsEditing(false);
        // Reset edited values
        setEditedName(product.name);
        setEditedPrice(product.price);
    };
    return (
        <li className="list-group-item">
            {isEditing ? (
                <div className="row">
                    <div className="col">
                        <input type="text" className="form-control" value={editedName} onChange={e => setEditedName(e.target.value)} required />
                    </div>
                    <div className="col">
                        <input type="number" className="form-control" value={editedPrice} onChange={e => setEditedPrice(e.target.value)} required />
                    </div>
                    <div className="col-auto">
                        <button className="btn btn-success me-2" onClick={handleSave}>Save</button>
                        <button className="btn btn-secondary" onClick={handleCancel}>Cancel</button>
                    </div>
                </div>
            ) : (
                <div className="d-flex justify-content-between align-items-center">
                    <span>{product.name} - ${product.price}</span>
                    <div>
                        <button className="btn btn-danger me-2" onClick={onDelete}>Delete</button>
                        <button className="btn btn-primary" onClick={handleEdit}>Edit</button>
                    </div>
                </div>
            )}
        </li>
    );
};
export default ProductListItem;

Product service

// src/services/productService.js
import axios from 'axios';
const baseURL = 'https://localhost:7202/api/Product';
const productService = {
    getAllProducts: async () => {
        const response = await axios.get(baseURL);
        return response.data;
    },
    addProduct: async (product) => {
        const response = await axios.post(baseURL, product);
        return response.data;
    },
    deleteProduct: async (id) => {
        const response = await axios.delete(`${baseURL}/${id}`);
        return response.data;
    },
    updateProduct: async (id, product) => {
        const response = await axios.put(`${baseURL}/${id}`, product);
        return response.data;
    }
};
export default productService;

App component

// src/App.js
import React, { useState } from 'react';
import ProductList from './components/ProductList/ProductList';
import ProductForm from './components/ProductForm/ProductForm';
function App() {
    const [refresh, setRefresh] = useState(false);
    const handleProductAdded = () => {
        setRefresh(!refresh); // Toggle refresh state to trigger re-render
    };
    return (
        <div>
            <ProductList key={refresh} />
            <ProductForm onProductAdded={handleProductAdded} />
        </div>
    );
}
export default App;

Step 5. Run the application using the following command and perform the different CRUD operations with the help of the same.

CRUD operations

Docker Files for Application
 

Docker file for backend application (.NET Core)

# Use the official .NET Core SDK as a parent image
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build

WORKDIR /app

# Copy the project file and restore any dependencies (use .csproj for the project name)
COPY *.csproj ./
RUN dotnet restore

# Copy the rest of the application code
COPY . .

# Publish the application
RUN dotnet publish -c Release -o out

# Build the runtime image
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime

WORKDIR /app
COPY --from=build /app/out ./

# Expose the port your application will run on
EXPOSE 80

# Start the application
ENTRYPOINT ["dotnet", "ProductManagementAPI.dll"]
  • Line 1-2: Uses the official .NET Core SDK image (mcr.microsoft.com/dotnet/sdk:6.0) as a base.
  • Line 4: Sets the working directory to /app.
  • Line 6-7: Copies the project file(s) (*.csproj) into the container.
  • Line 9: Runs dotnet restore to restore dependencies specified in the project file(s).
  • Line 11-12: Copies the rest of the application code into the container.
  • Line 14-15: Publishes the application in Release configuration (dotnet publish -c Release -o out), outputting to the out directory.
  • Line 17-18: Uses the official .NET Core ASP.NET runtime image (mcr.microsoft.com/dotnet/aspnet:6.0) as a base.
  • Line 20-21: Sets the working directory to /app and Copies the published output from the build stage (from /app/out) into the /app directory of the runtime stage.
  • Line 23-24: Exposes port 80 to allow external access to the application.
  • Line 26-27: Specifies dotnet ProductManagementAPI.dll as the entry point command to start the application.

Docker file for frontend application (React JS)

# Use the official Node.js base image
FROM node:18

# Set the working directory
WORKDIR /app

# Copy package.json and package-lock.json files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application code
COPY . .

# Build the React app
ARG REACT_APP_API_URL
ENV REACT_APP_API_URL=$REACT_APP_API_URL
RUN npm run build

# Install serve globally to serve the build folder
RUN npm install -g serve

# Expose the port the app runs on
EXPOSE 3000

# Start the React app
CMD ["serve", "-s", "build"]
  • Line 1-2: specifies the base image, using Node.js version 18.
  • Line 4-5: sets /app as the working directory for subsequent commands.
  • Line 7-8: This copies package.json and package-lock.json from your local machine to the Docker image.
  • Line 10-11: runs npm install to install all dependencies listed in package.json.
  • Line 13-14: Copy the rest of your application code to the Docker image.
  • Line 16-19: The ARG instruction defines a build-time variable REACT_APP_API_URL, The ENV instruction sets an environment variable REACT_APP_API_URL with the value of the build-time variable. Also, the RUN npm run build builds the React application.
  • Line 21-22: installs the serve package globally to serve the built React application.
  • Line 24-25: This exposes port 3000, which is where the application will run.
  • Line 27-28: The command starts the React application using serve to serve the build folder.

For front-end applications, Create a .env file in the root of your React project. This file will hold your environment variables, which you can use while running your Docker image and pass the back-end API URL.

REACT_APP_API_URL=http://your-backend-url.com

Next, modify your backend hard-coded URL in the product service.

// src/services/productService.js
import axios from 'axios';
const baseURL = process.env.REACT_APP_API_URL;
const productService = {
    getAllProducts: async () => {
        const response = await axios.get(baseURL);
        return response.data;
    },
    addProduct: async (product) => {
        const response = await axios.post(baseURL, product);
        return response.data;
    },
    deleteProduct: async (id) => {
        const response = await axios.delete(`${baseURL}/${id}`);
        return response.data;
    },
    updateProduct: async (id, product) => {
        const response = await axios.put(`${baseURL}/${id}`, product);
        return response.data;
    }
};
export default productService;

Containerize the front-end and back-end application

Step 1. Build the docker images.

docker build -t productbackendapp:latest .

Build

docker build --build-arg REACT_APP_API_URL=http://localhost:8085/api/Product -t productfrontendapp .

Code

Step 2. Run the images after passing the required parameters, like env and arguments.

docker run -p 8085:80 -e "ConnectionStrings__DefaultConnection=Data Source=192.168.100.194,1433;Initial Catalog=ReactNetCoreCrudDb;User Id=sa;Password=database@1;" productbackendapp:latest

Required parameters

Images

Containers

API

Product

docker run -p 3000:3000 productfrontendapp

React JS

Local

Volume

Sample Application Screenshots

Sample Application

Product List

Add Product

Github

GitHub

Conclusion

In this article, we created a product management backend application using .NET Core and SQL Server with different API endpoints that are required to perform CRUD operations. Later on, I created the front-end application using React JS and consumed the back-end application inside the same with the help of Axios. Also, we containerized both applications with the help of Docker.


Similar Articles