Building a React CRUD App with a .NET API
CRUD (Create, Read, Update, Delete) operations are essential for any dynamic web application. In this article, we'll guide you through building a CRUD app using React on the front end and a .NET API on the back end, demonstrating how these technologies work together seamlessly.
Prerequisites
- Node.js
- .NET 8
- MySQL
Setup React project
npm create vite@4.4.0 view -- --template react
cd view
npm install react-router-dom@5 axios
React project structure
├─ index.html
├─ public
│ └─ css
│ └─ style.css
└─ src
├─ components
│ └─ product
│ ├─ Create.jsx
│ ├─ Delete.jsx
│ ├─ Detail.jsx
│ ├─ Edit.jsx
│ ├─ Index.jsx
│ └─ Service.js
├─ history.js
├─ http.js
├─ main.jsx
├─ router.jsx
└─ App.jsx
React project files
main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<App />
)
The main.jsx
file is the entry point for a React app. It imports React, ReactDOM, and the App
component. The ReactDOM.createRoot
method is used to render the App
component into the HTML element with the ID root
.
App.jsx
import React, { useState, useEffect } from 'react'
import { Router, Link } from 'react-router-dom'
import history from './history'
import Route from './router'
export default function App() {
return (
<Router history={history}>
<Route />
</Router>
)
}
The App.jsx
file sets up routing for a React app. The App
component wraps the application in a Router
using the custom history
, and renders the Route
component for handling routes.
history.js
import { createBrowserHistory } from 'history'
export default createBrowserHistory()
The history.js
file exports a custom browser history object created with createBrowserHistory
for managing navigation in a React app.
router.jsx
import React, { Suspense, lazy } from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
export default function AppRoute(props) {
return (
<Suspense fallback={''}>
<Switch>
<Route path="/" component={(p) => <Redirect to="/product" /> } exact />
<Route path="/product" component={lazy(() => import('./components/product/Index'))} exact />
<Route path="/product/create" component={lazy(() => import('./components/product/Create'))} exact />
<Route path="/product/:id/" component={lazy(() => import('./components/product/Detail'))} exact />
<Route path="/product/edit/:id/" component={lazy(() => import('./components/product/Edit'))} exact />
<Route path="/product/delete/:id/" component={lazy(() => import('./components/product/Delete'))} exact />
</Switch>
</Suspense>
)
}
The router.jsx
file sets up routing for a React app with lazy-loaded components. It uses Suspense
to handle loading status, and Switch
to define routes. The root path redirects to /product
, and specific routes handle product-related pages like create, detail, edit
, and delete
.
http.js
import axios from 'axios'
let http = axios.create({
baseURL: 'http://localhost:5122/api',
headers: {
'Content-type': 'application/json'
}
})
export default http
The http.js
file configures and exports an Axios instance with a centralized base URL, which is a standard practice for managing API endpoints and default headers set to application/json
.
Create.jsx
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'
export default function ProductCreate(props) {
const [ product, setProduct ] = useState({})
function create(e) {
e.preventDefault()
Service.create(product).then(() => {
props.history.push('/product')
}).catch((e) => {
alert(e.response.data)
})
}
function onChange(e) {
let data = { ...product }
data[e.target.name] = e.target.value
setProduct(data)
}
return (
<div className="container">
<div className="row">
<div className="col">
<form method="post" onSubmit={create}>
<div className="row">
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_name">Name</label>
<input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name ?? '' } maxLength="50" />
</div>
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_price">Price</label>
<input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price ?? '' } type="number" />
</div>
<div className="col-12">
<Link className="btn btn-secondary" to="/product">Cancel</Link>
<button className="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
)
}
The create.jsx
file defines a ProductCreate
component for adding a new product. It uses useState
to manage form data, and handles form submission with create(e)
that sends data via Service.create
and redirects on success. The form includes fields for product name and price, with a cancel link and submit button.
Delete.jsx
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'
export default function ProductDelete(props) {
const [ product, setProduct ] = useState({})
useEffect(() => {
get()
}, [ props.match.params.id ])
function get() {
return Service.delete(props.match.params.id).then(response => {
setProduct(response.data)
}).catch(e => {
alert(e.response.data)
})
}
function remove(e) {
e.preventDefault()
Service.delete(props.match.params.id, product).then(() => {
props.history.push('/product')
}).catch((e) => {
alert(e.response.data)
})
}
return (
<div className="container">
<div className="row">
<div className="col">
<form method="post" onSubmit={remove}>
<div className="row">
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_id">Id</label>
<input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />
</div>
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_name">Name</label>
<input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />
</div>
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_price">Price</label>
<input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />
</div>
<div className="col-12">
<Link className="btn btn-secondary" to="/product">Cancel</Link>
<button className="btn btn-danger">Delete</button>
</div>
</div>
</form>
</div>
</div>
</div>
)
}
The Delete.jsx
file defines a ProductDelete
component for deleting a product. It fetches product details using Service.delete
and displays them in a read-only form. The component uses useEffect
to load product data based on the product ID from the route parameters. The remove(e)
function handles deletion and redirects to /product
upon success.
Detail.jsx
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'
export default function ProductDetail(props) {
const [ product, setProduct ] = useState({})
useEffect(() => {
get()
}, [ props.match.params.id ])
function get() {
return Service.get(props.match.params.id).then(response => {
setProduct(response.data)
}).catch(e => {
alert(e.response.data)
})
}
return (
<div className="container">
<div className="row">
<div className="col">
<form method="post">
<div className="row">
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_id">Id</label>
<input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />
</div>
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_name">Name</label>
<input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />
</div>
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_price">Price</label>
<input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />
</div>
<div className="col-12">
<Link className="btn btn-secondary" to="/product">Back</Link>
<Link className="btn btn-primary" to={`/product/edit/${product.id}`}>Edit</Link>
</div>
</div>
</form>
</div>
</div>
</div>
)
}
The Detail.jsx
file defines a ProductDetail
component that displays details of a product. It fetches product data using Service.get
based on the product ID from the route parameters and displays it in a read-only form, and provides links to go back to the product list or to edit the product.
Edit.jsx
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'
export default function ProductEdit(props) {
const [ product, setProduct ] = useState({})
useEffect(() => {
get()
}, [ props.match.params.id ])
function get() {
return Service.edit(props.match.params.id).then(response => {
setProduct(response.data)
}).catch(e => {
alert(e.response.data)
})
}
function edit(e) {
e.preventDefault()
Service.edit(props.match.params.id, product).then(() => {
props.history.push('/product')
}).catch((e) => {
alert(e.response.data)
})
}
function onChange(e) {
let data = { ...product }
data[e.target.name] = e.target.value
setProduct(data)
}
return (
<div className="container">
<div className="row">
<div className="col">
<form method="post" onSubmit={edit}>
<div className="row">
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_id">Id</label>
<input readOnly id="product_id" name="id" className="form-control" onChange={onChange} value={product.id ?? '' } type="number" required />
</div>
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_name">Name</label>
<input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name ?? '' } maxLength="50" />
</div>
<div className="mb-3 col-md-6 col-lg-4">
<label className="form-label" htmlFor="product_price">Price</label>
<input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price ?? '' } type="number" />
</div>
<div className="col-12">
<Link className="btn btn-secondary" to="/product">Cancel</Link>
<button className="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
)
}
The Edit.jsx
file defines a ProductEdit
component for updating product details. It fetches the current product data using Service.edit
and populates a form with this data. The form allows users to modify the product's name and price. On form submission, the edit(e)
function updates the product via Service.edit
and redirects to the product list on success.
Index.jsx
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'
export default function ProductIndex(props) {
const [products, setProducts] = useState([])
useEffect(() => {
get()
}, [props.location])
function get() {
Service.get().then(response => {
setProducts(response.data)
}).catch(e => {
alert(e.response.data)
})
}
return (
<div className="container">
<div className="row">
<div className="col">
<table className="table table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{products.map((product, index) =>
<tr key={index}>
<td className="text-center">{product.id}</td>
<td>{product.name}</td>
<td className="text-center">{product.price}</td>
<td className="text-center">
<Link className="btn btn-secondary" to={`/product/${product.id}`} title="View"><i className="fa fa-eye"></i></Link>
<Link className="btn btn-primary" to={`/product/edit/${product.id}`} title="Edit"><i className="fa fa-pencil"></i></Link>
<Link className="btn btn-danger" to={`/product/delete/${product.id}`} title="Delete"><i className="fa fa-times"></i></Link>
</td>
</tr>
)}
</tbody>
</table>
<Link className="btn btn-primary" to="/product/create">Create</Link>
</div>
</div>
</div>
)
}
The Index.jsx
file defines a ProductIndex
component that displays a list of products in a table. It fetches product data using Service.get
and updates the list on component mount. The table shows product ID, name, and price, with action buttons for viewing, editing, and deleting each product. It also includes a link to create a new product.
Service.js
import http from '../../http'
export default {
get(id) {
if (id) {
return http.get(`/products/${id}`)
}
else {
return http.get('/products' + location.search)
}
},
create(data) {
if (data) {
return http.post('/products', data)
}
else {
return http.get('/products/create')
}
},
edit(id, data) {
if (data) {
return http.put(`/products/${id}`, data)
}
else {
return http.get(`/products/${id}`)
}
},
delete(id, data) {
if (data) {
return http.delete(`/products/${id}`)
}
else {
return http.get(`/products/${id}`)
}
}
}
The Service.js
file defines API methods for handling product operations. It uses an http
instance for making requests:
get(id)
Retrieves a single product by ID or all products if no ID is provided.create(data)
Creates a new product with the provided data or fetches the creation form if no data is provided.edit(id, data)
Updates a product by ID with the provided data or fetches the product details if no data is provided.delete(id, data)
Deletes a product by ID or fetches the product details if no data is provided.
style.css
.container {
margin-top: 2em;
}
.btn {
margin-right: 0.25em;
}
The CSS adjusts the layout by adding space above the container and spacing out buttons horizontally.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
The HTML serves as the main entry point for an React application, including Bootstrap for styling, Font Awesome for icons, It features a div
with the ID root
where the React app will render.
Setup .NET API project
dotnet new webapi -o api -n App
Create a testing database named "example" and execute the database.sql file to import the table and data.
.NET API Project structure
├─ Controllers
│ └─ ProductController.cs
├─ Models
│ ├─ DataContext.cs
│ └─ Product.cs
├─ Program.cs
├─ App.csproj
└─ appsettings.json
.NET API Project files
App.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.0" />
</ItemGroup>
</Project>
This file is the .NET project configuration file, where we have added the MySql.EntityFrameworkCore
package.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Database": "server=localhost;port=3306;database=example;user id=root;password=;"
}
}
This is the .NET application configuration file, which includes the database connection details.
Program.cs
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy => {
policy.AllowAnyOrigin();
policy.AllowAnyHeader();
policy.AllowAnyMethod();
});
});
builder.Services.AddDbContext<App.Models.DataContext>(options => options.UseMySQL(builder.Configuration.GetConnectionString("Database")));
var app = builder.Build();
app.UseCors();
app.UseRouting();
app.MapControllers();
app.Run();
The program.cs
file is the main entry point for the .NET application. It configures services, sets up CORS, establishes a MySQL database context, and defines middleware for routing before starting the application.
DataContext.cs
using Microsoft.EntityFrameworkCore;
namespace App.Models
{
public partial class DataContext : DbContext
{
public virtual DbSet<Product> Product { get; set; }
public DataContext()
{
}
public DataContext(DbContextOptions<DataContext> options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.ToTable("Product");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(50).IsUnicode(false);
entity.Property(e => e.Price).HasColumnName("price").HasColumnType("decimal(12,2)");
});
}
}
}
DataContext.cs
defines the Entity Framework Core context for the application, featuring a DbSet
Product
table. It also sets up the Product
entity with table mapping, primary key, and column details, including names, types, and constraints.
Product.cs
using System.ComponentModel.DataAnnotations;
namespace App.Models
{
public class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
Product.cs
defines the model that maps to the database table named Product
. It includes properties with annotations that specify how these properties correspond to the table's columns.
ProductController.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using App.Models;
namespace App.Controllers
{
public class ProductController : Controller
{
private readonly DataContext _context;
public ProductController(DataContext context)
{
_context = context;
}
[HttpGet("api/products")]
public async Task<IActionResult> Index()
{
var products = await _context.Product.ToListAsync();
return Ok(products);
}
[HttpGet("api/products/{id}")]
public async Task<IActionResult> Detail(int? id)
{
var product = await _context.Product.FirstOrDefaultAsync(e => e.Id == id);
return Ok(product);
}
[HttpPost("api/products")]
public async Task<IActionResult> Create([FromBody] Product model)
{
var product = new Product();
product.Id = model.Id;
product.Name = model.Name;
product.Price = model.Price;
_context.Add(product);
await _context.SaveChangesAsync();
return Ok(product);
}
[HttpPut("api/products/{id}")]
public async Task<IActionResult> Update(int id, [FromBody] Product model)
{
var product = await _context.Product.FirstOrDefaultAsync(e => e.Id == id);
product.Name = model.Name;
product.Price = model.Price;
await _context.SaveChangesAsync();
return Ok(product);
}
[HttpDelete("api/products/{id}")]
public async Task<IActionResult> Delete(int id)
{
var product = await _context.Product.FindAsync(id);
_context.Product.Remove(product);
await _context.SaveChangesAsync();
return Ok();
}
}
}
The ProductController
class provides API endpoints for managing Product
entities. It uses DataContext
to interact with the database:
Index
(GET) retrieves all products.Detail
(GET) fetches a specific product by ID.Create
(POST) adds a new product.Update
(PUT) modifies an existing product by ID.Delete
(DELETE) removes a product by ID.
Run projects
Run React project
npm run dev
Run .NET API project
dotnet run
Open the web browser and goto http://localhost:5173
You will find this product list page.
Testing
Click the "View" button to see the product details page.
Click the "Edit" button to modify the product and update its details.
Click the "Submit" button to save the updated product details.
Click the "Create" button to add a new product and input its details.
Click the "Submit" button to save the new product.
Click the "Delete" button to remove the previously created product.
Click the "Delete" button to confirm the removal of this product.
Conclusion
In conclusion, we have learned how to create a basic React project using JSX syntax to build views and routing to connect with a .NET API as the backend. By utilizing Entity Framework for database operations, we've developed a responsive front-end that seamlessly interacts with a robust backend. This combination lays a strong foundation for building modern, full-stack web applications.
Source code: https://github.com/stackpuz/Example-CRUD-React-18-dotnet-8
Create a React CRUD App in Minutes: https://stackpuz.com