.Net + PostgreSQL + React JS CRUD Application Example
In this section, we will learn how to develop a full-stack web application that is a basic User Management Application using .Net 6, PostgreSQL, Npgsql Entity Framework Core provider for PostgreSQL, and React JS. You could download the source code from our Github repository, the download link is provided at the end of this tutorial.
After completing this tutorial what we will build?
We divided this tutorial into two parts.
PART 1 - Restful API Development with .Net 6 & PostgreSQL
These are APIs that .Net backend App will export:
Create database and table
First, you need to create a database in the PostgreSQL server. You can use the following command to create a database in the PostgresSQL server:CREATE DATABASE testdb;
Create table:CREATE TABLE IF NOT EXISTS public.users( id serial primary key, email VARCHAR(40) not null, first_name VARCHAR(40) not null, last_name VARCHAR(40) not null);
CREATE DATABASE testdb;
CREATE TABLE IF NOT EXISTS public.users(id serial primary key,email VARCHAR(40) not null,first_name VARCHAR(40) not null,last_name VARCHAR(40) not null);
Backend project directory:
User Entity
Path: /Entities/User.cs
The user entity class represents the data stored in the database for users.
namespace WebApi.Entities;
using System.Text.Json.Serialization;
public class User{ public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } }
Path: /Entities/User.cs
The user entity class represents the data stored in the database for users.
namespace WebApi.Entities;
using System.Text.Json.Serialization;
public class User{ public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } }
App Exception
Path: /Helpers/AppException.cs
The app exception is a custom exception class used to differentiate between handled and unhandled exceptions in the .NET API. Handled exceptions are generated by application code and used to return custome error messages.
namespace WebApi.Helpers;
using System.Globalization;
public class AppException : Exception{ public AppException() : base() {}
public AppException(string message) : base(message) { }
public AppException(string message, params object[] args) : base(String.Format(CultureInfo.CurrentCulture, message, args)) { }}
Path: /Helpers/AppException.cs
The app exception is a custom exception class used to differentiate between handled and unhandled exceptions in the .NET API. Handled exceptions are generated by application code and used to return custome error messages.
namespace WebApi.Helpers;
using System.Globalization;
public class AppException : Exception{ public AppException() : base() {}
public AppException(string message) : base(message) { }
public AppException(string message, params object[] args) : base(String.Format(CultureInfo.CurrentCulture, message, args)) { }}
AutoMapper Profile
Path: /Helpers/AutoMapperProfile.cs
AutoMapper is an object mapper that helps you transform one object of one type into an output object of a different type. This type of transformation is often needed when you work on business logic. In this example we're using it to map between User entities and a couple of different model types - CreateRequest and UpdateRequest.
namespace WebApi.Helpers;
using AutoMapper;using WebApi.Entities;using WebApi.Models.Users;
public class AutoMapperProfile : Profile{ public AutoMapperProfile() { // CreateRequest -> User CreateMap<CreateRequest, User>();
// UpdateRequest -> User CreateMap<UpdateRequest, User>() .ForAllMembers(x => x.Condition( (src, dest, prop) => { // ignore both null & empty string properties if (prop == null) return false; if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
return true; } )); }}
Path: /Helpers/AutoMapperProfile.cs
AutoMapper is an object mapper that helps you transform one object of one type into an output object of a different type. This type of transformation is often needed when you work on business logic. In this example we're using it to map between User entities and a couple of different model types - CreateRequest and UpdateRequest.
namespace WebApi.Helpers;
using AutoMapper;using WebApi.Entities;using WebApi.Models.Users;
public class AutoMapperProfile : Profile{ public AutoMapperProfile() { // CreateRequest -> User CreateMap<CreateRequest, User>();
// UpdateRequest -> User CreateMap<UpdateRequest, User>() .ForAllMembers(x => x.Condition( (src, dest, prop) => { // ignore both null & empty string properties if (prop == null) return false; if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
return true; } )); }}
Data Context
Path: /Helpers/DataContext.cs
The primary class that is responsible for interacting with data as objects is the DbContext. The recommended way to work with context is to define a class that derives from the DbContext and exposes the DbSet properties that represent collections of the specified entities in the context.
options.UseNpgsql(Configuration.GetConnectionString("ApiDatabase") configures Entity Framework to create and connect to an PostgreSQL database.
UseSnakeCaseNamingConvention(): This will automatically make all your table and column names have snake_case naming.
namespace WebApi.Helpers;
using Microsoft.EntityFrameworkCore;using WebApi.Entities;
public class DataContext : DbContext{ protected readonly IConfiguration Configuration;
public DataContext(IConfiguration configuration) { Configuration = configuration; }
protected override void OnConfiguring(DbContextOptionsBuilder options) { // connect to postgres with connection string from app settings options.UseNpgsql(Configuration.GetConnectionString("ApiDatabase")) .UseSnakeCaseNamingConvention(); }
public DbSet<User> Users { get; set; }}
Path: /Helpers/DataContext.cs
The primary class that is responsible for interacting with data as objects is the DbContext. The recommended way to work with context is to define a class that derives from the DbContext and exposes the DbSet properties that represent collections of the specified entities in the context.
options.UseNpgsql(Configuration.GetConnectionString("ApiDatabase") configures Entity Framework to create and connect to an PostgreSQL database.
UseSnakeCaseNamingConvention(): This will automatically make all your table and column names have snake_case naming.
namespace WebApi.Helpers;
using Microsoft.EntityFrameworkCore;using WebApi.Entities;
public class DataContext : DbContext{ protected readonly IConfiguration Configuration;
public DataContext(IConfiguration configuration) { Configuration = configuration; }
protected override void OnConfiguring(DbContextOptionsBuilder options) { // connect to postgres with connection string from app settings options.UseNpgsql(Configuration.GetConnectionString("ApiDatabase")) .UseSnakeCaseNamingConvention(); }
public DbSet<User> Users { get; set; }}
Global Error Handler Middleware
Path: /Helpers/ErrorHandlerMiddleware.cs
Using the global error handler, we can extract all the exception handling logic into a single centralized place. It's configured as middleware in the Program.cs file.
namespace WebApi.Helpers;
using Microsoft.AspNetCore.Http;using Microsoft.Extensions.Logging;using System;using System.Collections.Generic;using System.Net;using System.Text.Json;using System.Threading.Tasks;
public class ErrorHandlerMiddleware{ private readonly RequestDelegate _next; private readonly ILogger _logger;
public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger) { _next = next; _logger = logger; }
public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception error) { var response = context.Response; response.ContentType = "application/json";
switch (error) { case AppException e: // custom application error response.StatusCode = (int)HttpStatusCode.BadRequest; break; case KeyNotFoundException e: // not found error response.StatusCode = (int)HttpStatusCode.NotFound; break; default: // unhandled error _logger.LogError(error, error.Message); response.StatusCode = (int)HttpStatusCode.InternalServerError; break; }
var result = JsonSerializer.Serialize(new { message = error?.Message }); await response.WriteAsync(result); } }}
Path: /Helpers/ErrorHandlerMiddleware.cs
Using the global error handler, we can extract all the exception handling logic into a single centralized place. It's configured as middleware in the Program.cs file.
namespace WebApi.Helpers;
using Microsoft.AspNetCore.Http;using Microsoft.Extensions.Logging;using System;using System.Collections.Generic;using System.Net;using System.Text.Json;using System.Threading.Tasks;
public class ErrorHandlerMiddleware{ private readonly RequestDelegate _next; private readonly ILogger _logger;
public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger) { _next = next; _logger = logger; }
public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception error) { var response = context.Response; response.ContentType = "application/json";
switch (error) { case AppException e: // custom application error response.StatusCode = (int)HttpStatusCode.BadRequest; break; case KeyNotFoundException e: // not found error response.StatusCode = (int)HttpStatusCode.NotFound; break; default: // unhandled error _logger.LogError(error, error.Message); response.StatusCode = (int)HttpStatusCode.InternalServerError; break; }
var result = JsonSerializer.Serialize(new { message = error?.Message }); await response.WriteAsync(result); } }}
Interface User Service
Path: /Services/IUserService.cs
The user service is responsible for all database interaction and core business logic related to user CRUD operations.
namespace WebApi.Services;
using WebApi.Entities;using WebApi.Models.Users;
public interface IUserService{ IEnumerable<User> GetAll(); User GetById(int id); void Create(CreateRequest model); void Update(int id, UpdateRequest model); void Delete(int id);}
Path: /Services/IUserService.cs
The user service is responsible for all database interaction and core business logic related to user CRUD operations.
namespace WebApi.Services;
using WebApi.Entities;using WebApi.Models.Users;
public interface IUserService{ IEnumerable<User> GetAll(); User GetById(int id); void Create(CreateRequest model); void Update(int id, UpdateRequest model); void Delete(int id);}
User Service Implementation
Path: /Services/UserService.csThe UserService.cs is a concrete user service class that implements the interface.
using AutoMapper;using WebApi.Entities;using WebApi.Helpers;using WebApi.Models.Users;
namespace WebApi.Services{ public class UserService : IUserService { private DataContext _context; private readonly IMapper _mapper;
public UserService( DataContext context, IMapper mapper) { _context = context; _mapper = mapper; }
public IEnumerable<User> GetAll() { return _context.Users; }
public User GetById(int id) { return getUser(id); }
public void Create(CreateRequest model) { // validate if (_context.Users.Any(x => x.Email == model.Email)) throw new AppException("User with the email '" + model.Email + "' already exists");
// map model to new user object var user = _mapper.Map<User>(model);
// save user _context.Users.Add(user); _context.SaveChanges(); }
public void Update(int id, UpdateRequest model) { var user = getUser(id);
// validate if (model.Email != user.Email && _context.Users.Any(x => x.Email == model.Email)) throw new AppException("User with the email '" + model.Email + "' already exists");
// copy model to user and save _mapper.Map(model, user); _context.Users.Update(user); _context.SaveChanges(); }
public void Delete(int id) { var user = getUser(id); _context.Users.Remove(user); _context.SaveChanges(); }
// helper methods
private User getUser(int id) { var user = _context.Users.Find(id); if (user == null) throw new KeyNotFoundException("User not found"); return user; } }}
The UserService.cs is a concrete user service class that implements the interface.
using AutoMapper;using WebApi.Entities;using WebApi.Helpers;using WebApi.Models.Users;
namespace WebApi.Services{ public class UserService : IUserService { private DataContext _context; private readonly IMapper _mapper;
public UserService( DataContext context, IMapper mapper) { _context = context; _mapper = mapper; }
public IEnumerable<User> GetAll() { return _context.Users; }
public User GetById(int id) { return getUser(id); }
public void Create(CreateRequest model) { // validate if (_context.Users.Any(x => x.Email == model.Email)) throw new AppException("User with the email '" + model.Email + "' already exists");
// map model to new user object var user = _mapper.Map<User>(model);
// save user _context.Users.Add(user); _context.SaveChanges(); }
public void Update(int id, UpdateRequest model) { var user = getUser(id);
// validate if (model.Email != user.Email && _context.Users.Any(x => x.Email == model.Email)) throw new AppException("User with the email '" + model.Email + "' already exists");
// copy model to user and save _mapper.Map(model, user); _context.Users.Update(user); _context.SaveChanges(); }
public void Delete(int id) { var user = getUser(id); _context.Users.Remove(user); _context.SaveChanges(); }
// helper methods
private User getUser(int id) { var user = _context.Users.Find(id); if (user == null) throw new KeyNotFoundException("User not found"); return user; } }}
CreateRequest.cs
Path: /Models/Users/CreateRequest.cs
namespace WebApi.Models.Users;
using System.ComponentModel.DataAnnotations;using WebApi.Entities;
public class CreateRequest{
[Required] public string FirstName { get; set; }
[Required] public string LastName { get; set; }
[Required] [EmailAddress] public string Email { get; set; }
}
Path: /Models/Users/CreateRequest.cs
namespace WebApi.Models.Users;
using System.ComponentModel.DataAnnotations;using WebApi.Entities;
public class CreateRequest{
[Required] public string FirstName { get; set; }
[Required] public string LastName { get; set; }
[Required] [EmailAddress] public string Email { get; set; }
}
UpdateRequest.cs
Path: /Models/Users/UpdateRequest.cs
namespace WebApi.Models.Users;
using System.ComponentModel.DataAnnotations;using WebApi.Entities;
public class UpdateRequest{ public string FirstName { get; set; } public string LastName { get; set; }
[EmailAddress] public string Email { get; set; }
private string replaceEmptyWithNull(string value) { return string.IsNullOrEmpty(value) ? null : value; }}
Path: /Models/Users/UpdateRequest.cs
namespace WebApi.Models.Users;
using System.ComponentModel.DataAnnotations;using WebApi.Entities;
public class UpdateRequest{ public string FirstName { get; set; } public string LastName { get; set; }
[EmailAddress] public string Email { get; set; }
private string replaceEmptyWithNull(string value) { return string.IsNullOrEmpty(value) ? null : value; }}
Users Controller
Path: /Controllers/UsersController.cs
The controller defines and handles all routes / endpoints for the api that relate to users, this includes standard CRUD operations for retrieving, updating, creating and deleting users.
namespace WebApi.Controllers;
using AutoMapper;using Microsoft.AspNetCore.Mvc;using WebApi.Models.Users;using WebApi.Services;
[ApiController][Route("api/v1/[controller]")]public class UsersController : ControllerBase{ private IUserService _userService; private IMapper _mapper;
public UsersController( IUserService userService, IMapper mapper) { _userService = userService; _mapper = mapper; }
[HttpGet] public IActionResult GetAll() { var users = _userService.GetAll(); return Ok(users); }
[HttpGet("{id}")] public IActionResult GetById(int id) { var user = _userService.GetById(id); return Ok(user); }
[HttpPost] public IActionResult Create(CreateRequest model) { _userService.Create(model); return Ok(new { message = "User created" }); }
[HttpPut("{id}")] public IActionResult Update(int id, UpdateRequest model) { _userService.Update(id, model); return Ok(new { message = "User updated" }); }
[HttpDelete("{id}")] public IActionResult Delete(int id) { _userService.Delete(id); return Ok(new { message = "User deleted" }); }}
Path: /Controllers/UsersController.cs
The controller defines and handles all routes / endpoints for the api that relate to users, this includes standard CRUD operations for retrieving, updating, creating and deleting users.
namespace WebApi.Controllers;
using AutoMapper;using Microsoft.AspNetCore.Mvc;using WebApi.Models.Users;using WebApi.Services;
[ApiController][Route("api/v1/[controller]")]public class UsersController : ControllerBase{ private IUserService _userService; private IMapper _mapper;
public UsersController( IUserService userService, IMapper mapper) { _userService = userService; _mapper = mapper; }
[HttpGet] public IActionResult GetAll() { var users = _userService.GetAll(); return Ok(users); }
[HttpGet("{id}")] public IActionResult GetById(int id) { var user = _userService.GetById(id); return Ok(user); }
[HttpPost] public IActionResult Create(CreateRequest model) { _userService.Create(model); return Ok(new { message = "User created" }); }
[HttpPut("{id}")] public IActionResult Update(int id, UpdateRequest model) { _userService.Update(id, model); return Ok(new { message = "User updated" }); }
[HttpDelete("{id}")] public IActionResult Delete(int id) { _userService.Delete(id); return Ok(new { message = "User deleted" }); }}
.NET 6 Program
Path: /Program.cs
Program.cs is the entry point of our app, where we configure the Web host. The program class configures the application infrastructure like Web host, logging, Dependency injection container, IIS integration, etc.
using System.Text.Json.Serialization;using WebApi.Helpers;using WebApi.Services;
var builder = WebApplication.CreateBuilder(args);
// add services to DI container{ var services = builder.Services; var env = builder.Environment; services.AddDbContext<DataContext>(); services.AddCors(); services.AddControllers().AddJsonOptions(x => { // serialize enums as strings in api responses (e.g. Role) x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// ignore omitted parameters on models to enable optional params (e.g. User update) x.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// configure DI for application services services.AddScoped<IUserService, UserService>();}
var app = builder.Build();
// configure HTTP request pipeline{ // global cors policy app.UseCors(x => x .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader());
// global error handler app.UseMiddleware<ErrorHandlerMiddleware>();
app.MapControllers();}
app.Run("http://localhost:9080");
Path: /Program.cs
Program.cs is the entry point of our app, where we configure the Web host. The program class configures the application infrastructure like Web host, logging, Dependency injection container, IIS integration, etc.
using System.Text.Json.Serialization;using WebApi.Helpers;using WebApi.Services;
var builder = WebApplication.CreateBuilder(args);
// add services to DI container{ var services = builder.Services; var env = builder.Environment; services.AddDbContext<DataContext>(); services.AddCors(); services.AddControllers().AddJsonOptions(x => { // serialize enums as strings in api responses (e.g. Role) x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// ignore omitted parameters on models to enable optional params (e.g. User update) x.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// configure DI for application services services.AddScoped<IUserService, UserService>();}
var app = builder.Build();
// configure HTTP request pipeline{ // global cors policy app.UseCors(x => x .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader());
// global error handler app.UseMiddleware<ErrorHandlerMiddleware>();
app.MapControllers();}
app.Run("http://localhost:9080");
.NET App Settings JSON
Path: /appsettings.json
The appsettings.json file is an application configuration file used to store configuration settings such as database connections strings, any application scope global variables, etc.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "ApiDatabase": "Host=localhost:5432; Database=testdb; Username=postgres; Password=root" }}
Path: /appsettings.json
The appsettings.json file is an application configuration file used to store configuration settings such as database connections strings, any application scope global variables, etc.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "ApiDatabase": "Host=localhost:5432; Database=testdb; Username=postgres; Password=root" }}
WebApi.csproj
Path: /WebApi.csproj
The csproj (C# project) is an MSBuild based file that contains target framework and NuGet package dependency information for the application.
The EFCore.NamingConventions plugin to automatically set all your table and column names to snake_case instead.
The Npgsql.EntityFrameworkCore.PostgreSQL is an Entity Framework Core provider built on top of Npgsql. It allows you to use the EF Core O/RM with PostreSQL.
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="AutoMapper" Version="11.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="7.0.2" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.1" /> </ItemGroup></Project>
Path: /WebApi.csproj
The csproj (C# project) is an MSBuild based file that contains target framework and NuGet package dependency information for the application.
The EFCore.NamingConventions plugin to automatically set all your table and column names to snake_case instead.
The Npgsql.EntityFrameworkCore.PostgreSQL is an Entity Framework Core provider built on top of Npgsql. It allows you to use the EF Core O/RM with PostreSQL.
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="AutoMapper" Version="11.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="7.0.2" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.1" /> </ItemGroup></Project>
Run the application and verify REST APIs
You can start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:9080.
OR
You can also start the application in debug mode in Visual Studio by opening the project root folder in Visual Studio and pressing F5 or by selecting Debug -> Start Debugging from the top menu, running in debug mode.
Add User:
Update User by ID:
Fetch all Users:
Get User by ID:
Delete User by ID:
You can start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:9080.
OR
You can also start the application in debug mode in Visual Studio by opening the project root folder in Visual Studio and pressing F5 or by selecting Debug -> Start Debugging from the top menu, running in debug mode.
Add User:
Update User by ID:
Fetch all Users:
Get User by ID:
Delete User by ID:
PART 2 - UI development using React JS
Frontend Project Directory
package.json
Path: /package.json
The package.json is a JSON file that is added in the root directory of your project. It contains human-readable metadata about the project as well as functional metadata like the package version number and a list of dependencies required by the application.
{ "name": "react-crud-app", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "axios": "^0.27.2", "bootstrap": "^4.5.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "react-scripts": "^5.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }}
Path: /package.json
The package.json is a JSON file that is added in the root directory of your project. It contains human-readable metadata about the project as well as functional metadata like the package version number and a list of dependencies required by the application.
{ "name": "react-crud-app", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "axios": "^0.27.2", "bootstrap": "^4.5.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "react-scripts": "^5.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }}
Create Components
A Component is one of the core building blocks of React. In other words, we can say that every application you will develop in React will be made up of pieces called components. Components make the task of building UIs much easier. You can see a UI broken down into multiple individual pieces called components and work on them independently and merge them all in a parent component which will be your final UI.
A Component is one of the core building blocks of React. In other words, we can say that every application you will develop in React will be made up of pieces called components. Components make the task of building UIs much easier. You can see a UI broken down into multiple individual pieces called components and work on them independently and merge them all in a parent component which will be your final UI.
CreateUserComponent.jsx
Path: /components/CreateUserComponent.jsximport React, { Component } from 'react'import UserService from '../services/UserService';
class CreateUserComponent extends Component { constructor(props) { super(props) this.state = { id: this.props.match.params.id, firstName: '', lastName: '', email: '', errorMessage: '' } this.changeFirstNameHandler = this.changeFirstNameHandler.bind(this); this.changeLastNameHandler = this.changeLastNameHandler.bind(this); this.saveOrUpdateUser = this.saveOrUpdateUser.bind(this); }
componentDidMount() { if (this.state.id === '_add') { return } else { UserService.getUserById(this.state.id).then((res) => { let user = res.data; this.setState({ firstName: user.firstName, lastName: user.lastName, email: user.email }); }); } } saveOrUpdateUser = (e) => { e.preventDefault(); let user = { firstName: this.state.firstName, lastName: this.state.lastName, email: this.state.email }; console.log('user => ' + JSON.stringify(user)); if (this.state.id === '_add') { UserService.createUser(user).then(res => { this.props.history.push('/users'); },err => this.setState({errorMessage: err.message})); } else { UserService.updateUser(user, this.state.id).then(res => { this.props.history.push('/users'); },err => this.setState({errorMessage: err.message})); } }
changeFirstNameHandler = (event) => { this.setState({ firstName: event.target.value }); }
changeLastNameHandler = (event) => { this.setState({ lastName: event.target.value }); }
changeEmailHandler = (event) => { this.setState({ email: event.target.value }); }
cancel() { this.props.history.push('/users'); }
getTitle() { if (this.state.id === '_add') { return <h3 className="text-center">Add User</h3> } else { return <h3 className="text-center">Update User</h3> } } render() { return ( <div> <br></br> <div className="container"> <div className="row"> <div className="card col-md-6 offset-md-3 offset-md-3"> { this.getTitle() } <div className="card-body"> <form> <div className="form-group"> <label> First Name: </label><input placeholder="First Name" name="firstName" className="form-control" value={this.state.firstName} onChange={this.changeFirstNameHandler} /> </div> <div className="form-group"> <label> Last Name: </label><input placeholder="Last Name" name="lastName" className="form-control" value={this.state.lastName} onChange={this.changeLastNameHandler} /> </div> <div className="form-group"> <label> Email Id: </label><input placeholder="Email Address" name="email" className="form-control" value={this.state.email} onChange={this.changeEmailHandler} /> </div>
<button className="btn btn-success" onClick={this.saveOrUpdateUser}>Save</button><button className="btn btn-danger" onClick={this.cancel.bind(this)} style={{ marginLeft: "10px" }}>Cancel</button> { this.state.errorMessage && <h5 className="alert alert-danger"> { this.state.errorMessage } </h5> } </form> </div> </div> </div>
</div> </div> ) }}
export default CreateUserComponent
import React, { Component } from 'react'import UserService from '../services/UserService';
class CreateUserComponent extends Component { constructor(props) { super(props) this.state = { id: this.props.match.params.id, firstName: '', lastName: '', email: '', errorMessage: '' } this.changeFirstNameHandler = this.changeFirstNameHandler.bind(this); this.changeLastNameHandler = this.changeLastNameHandler.bind(this); this.saveOrUpdateUser = this.saveOrUpdateUser.bind(this); }
componentDidMount() { if (this.state.id === '_add') { return } else { UserService.getUserById(this.state.id).then((res) => { let user = res.data; this.setState({ firstName: user.firstName, lastName: user.lastName, email: user.email }); }); } } saveOrUpdateUser = (e) => { e.preventDefault(); let user = { firstName: this.state.firstName, lastName: this.state.lastName, email: this.state.email }; console.log('user => ' + JSON.stringify(user)); if (this.state.id === '_add') { UserService.createUser(user).then(res => { this.props.history.push('/users'); },err => this.setState({errorMessage: err.message})); } else { UserService.updateUser(user, this.state.id).then(res => { this.props.history.push('/users'); },err => this.setState({errorMessage: err.message})); } }
changeFirstNameHandler = (event) => { this.setState({ firstName: event.target.value }); }
changeLastNameHandler = (event) => { this.setState({ lastName: event.target.value }); }
changeEmailHandler = (event) => { this.setState({ email: event.target.value }); }
cancel() { this.props.history.push('/users'); }
getTitle() { if (this.state.id === '_add') { return <h3 className="text-center">Add User</h3> } else { return <h3 className="text-center">Update User</h3> } } render() { return ( <div> <br></br> <div className="container"> <div className="row"> <div className="card col-md-6 offset-md-3 offset-md-3"> { this.getTitle() } <div className="card-body"> <form> <div className="form-group"> <label> First Name: </label><input placeholder="First Name" name="firstName" className="form-control" value={this.state.firstName} onChange={this.changeFirstNameHandler} /> </div> <div className="form-group"> <label> Last Name: </label><input placeholder="Last Name" name="lastName" className="form-control" value={this.state.lastName} onChange={this.changeLastNameHandler} /> </div> <div className="form-group"> <label> Email Id: </label><input placeholder="Email Address" name="email" className="form-control" value={this.state.email} onChange={this.changeEmailHandler} /> </div>
<button className="btn btn-success" onClick={this.saveOrUpdateUser}>Save</button><button className="btn btn-danger" onClick={this.cancel.bind(this)} style={{ marginLeft: "10px" }}>Cancel</button> { this.state.errorMessage && <h5 className="alert alert-danger"> { this.state.errorMessage } </h5> } </form> </div> </div> </div>
</div> </div> ) }}
export default CreateUserComponent
ListUserComponent.jsx
Path: /components/ListUserComponent.jsximport React, { Component } from 'react'import UserService from '../services/UserService'
class ListUserComponent extends Component { constructor(props) { super(props)
this.state = { users: [] } this.addUser = this.addUser.bind(this); this.editUser = this.editUser.bind(this); this.deleteUser = this.deleteUser.bind(this); }
deleteUser(id){ UserService.deleteUser(id).then( res => { this.setState({users: this.state.users.filter(user => user.id !== id)}); }); } viewUser(id){ this.props.history.push(`/view-user/${id}`); } editUser(id){ this.props.history.push(`/add-user/${id}`); }
componentDidMount(){ UserService.getUsers().then((res) => { if(res.data==null) { this.props.history.push('/add-user/_add'); } this.setState({ users: res.data}); }); }
addUser(){ this.props.history.push('/add-user/_add'); }
render() { return ( <div> <h2 className="text-center">Users List</h2> <div className = "row"> <button className="btn btn-primary" onClick={this.addUser}> Add User</button> </div> <br></br> <div className = "row"> <table className = "table table-striped table-bordered">
<thead> <tr> <th> User First Name</th> <th> User Last Name</th> <th> User Email Id</th> <th> Actions</th> </tr> </thead> <tbody> { this.state.users.map( user => <tr key = {user.id}> <td> { user.firstName} </td> <td> {user.lastName}</td> <td> {user.email}</td> <td> <button onClick={ () => this.editUser(user.id)} className="btn btn-info">Update </button> <button style={{marginLeft: "10px"}} onClick={ () => this.deleteUser(user.id)} className="btn btn-danger">Delete </button> <button style={{marginLeft: "10px"}} onClick={ () => this.viewUser(user.id)} className="btn btn-info">View </button> </td> </tr> ) } </tbody> </table> </div> </div> ) }}
export default ListUserComponent
import React, { Component } from 'react'import UserService from '../services/UserService'
class ListUserComponent extends Component { constructor(props) { super(props)
this.state = { users: [] } this.addUser = this.addUser.bind(this); this.editUser = this.editUser.bind(this); this.deleteUser = this.deleteUser.bind(this); }
deleteUser(id){ UserService.deleteUser(id).then( res => { this.setState({users: this.state.users.filter(user => user.id !== id)}); }); } viewUser(id){ this.props.history.push(`/view-user/${id}`); } editUser(id){ this.props.history.push(`/add-user/${id}`); }
componentDidMount(){ UserService.getUsers().then((res) => { if(res.data==null) { this.props.history.push('/add-user/_add'); } this.setState({ users: res.data}); }); }
addUser(){ this.props.history.push('/add-user/_add'); }
render() { return ( <div> <h2 className="text-center">Users List</h2> <div className = "row"> <button className="btn btn-primary" onClick={this.addUser}> Add User</button> </div> <br></br> <div className = "row"> <table className = "table table-striped table-bordered">
<thead> <tr> <th> User First Name</th> <th> User Last Name</th> <th> User Email Id</th> <th> Actions</th> </tr> </thead> <tbody> { this.state.users.map( user => <tr key = {user.id}> <td> { user.firstName} </td> <td> {user.lastName}</td> <td> {user.email}</td> <td> <button onClick={ () => this.editUser(user.id)} className="btn btn-info">Update </button> <button style={{marginLeft: "10px"}} onClick={ () => this.deleteUser(user.id)} className="btn btn-danger">Delete </button> <button style={{marginLeft: "10px"}} onClick={ () => this.viewUser(user.id)} className="btn btn-info">View </button> </td> </tr> ) } </tbody> </table> </div> </div> ) }}
export default ListUserComponent
ViewUserComponent.jsx
Path: /components/ViewUserComponent.jsximport React, { Component } from 'react'import UserService from '../services/UserService'
class ViewUserComponent extends Component { constructor(props) { super(props)
this.state = { id: this.props.match.params.id, user: {} } }
componentDidMount(){ UserService.getUserById(this.state.id).then( res => { this.setState({user: res.data}); }) }
render() { return ( <div> <br></br> <div className = "card col-md-6 offset-md-3"> <h3 className = "text-center"> View User Details</h3> <div className = "card-body"> <div className = "row"> <label> User First Name: </label> <div> { this.state.user.firstName }</div> </div> <div className = "row"> <label> User Last Name: </label> <div> { this.state.user.lastName }</div> </div> <div className = "row"> <label> User Email ID: </label> <div> { this.state.user.email }</div> </div> </div>
</div> </div> ) }}
export default ViewUserComponent
import React, { Component } from 'react'import UserService from '../services/UserService'
class ViewUserComponent extends Component { constructor(props) { super(props)
this.state = { id: this.props.match.params.id, user: {} } }
componentDidMount(){ UserService.getUserById(this.state.id).then( res => { this.setState({user: res.data}); }) }
render() { return ( <div> <br></br> <div className = "card col-md-6 offset-md-3"> <h3 className = "text-center"> View User Details</h3> <div className = "card-body"> <div className = "row"> <label> User First Name: </label> <div> { this.state.user.firstName }</div> </div> <div className = "row"> <label> User Last Name: </label> <div> { this.state.user.lastName }</div> </div> <div className = "row"> <label> User Email ID: </label> <div> { this.state.user.email }</div> </div> </div>
</div> </div> ) }}
export default ViewUserComponent
FooterComponent.jsx
Path: /components/FooterComponent.jsximport React, { Component } from 'react'
class FooterComponent extends Component { constructor(props) { super(props)
this.state = { } }
render() { return ( <div> <footer className = "footer"> <span className="text-muted"> All Rights Reserved 200X @Knowledgefactory.net</span> </footer> </div> ) }}
export default FooterComponent
import React, { Component } from 'react'
class FooterComponent extends Component { constructor(props) { super(props)
this.state = { } }
render() { return ( <div> <footer className = "footer"> <span className="text-muted"> All Rights Reserved 200X @Knowledgefactory.net</span> </footer> </div> ) }}
export default FooterComponent
HeaderComponent.jsx
Path: /components/HeaderComponent.jsximport React, { Component } from 'react'import'bootstrap/dist/css/bootstrap.min.css';
class HeaderComponent extends Component { constructor(props) { super(props)
this.state = { } }
render() { return ( <div> <header> <nav className="navbar navbar-dark bg-primary"> <div> <a href="/users" className="navbar-brand">User Management App </a></div> </nav> </header> </div> ) }}
export default HeaderComponent
import React, { Component } from 'react'import'bootstrap/dist/css/bootstrap.min.css';
class HeaderComponent extends Component { constructor(props) { super(props)
this.state = { } }
render() { return ( <div> <header> <nav className="navbar navbar-dark bg-primary"> <div> <a href="/users" className="navbar-brand">User Management App </a></div> </nav> </header> </div> ) }}
export default HeaderComponent
UserService.js
Path: /services/UserService.jsimport axios from 'axios';
const USER_API_BASE_URL = "http://localhost:9080/api/v1/users";
class UserService {
getUsers(){ return axios.get(USER_API_BASE_URL); }
createUser(user){ return axios.post(USER_API_BASE_URL, user); }
getUserById(userId){ return axios.get(USER_API_BASE_URL + '/' + userId); }
updateUser(user, userId){ return axios.put(USER_API_BASE_URL + '/' + userId, user); }
deleteUser(userId){ return axios.delete(USER_API_BASE_URL + '/' + userId); }}
export default new UserService()
import axios from 'axios';
const USER_API_BASE_URL = "http://localhost:9080/api/v1/users";
class UserService {
getUsers(){ return axios.get(USER_API_BASE_URL); }
createUser(user){ return axios.post(USER_API_BASE_URL, user); }
getUserById(userId){ return axios.get(USER_API_BASE_URL + '/' + userId); }
updateUser(user, userId){ return axios.put(USER_API_BASE_URL + '/' + userId, user); }
deleteUser(userId){ return axios.delete(USER_API_BASE_URL + '/' + userId); }}
export default new UserService()
App.js
Path: /App.jsimport React from 'react';import './App.css';import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'import ListUserComponent from './components/ListUserComponent';import HeaderComponent from './components/HeaderComponent';import FooterComponent from './components/FooterComponent';import CreateUserComponent from './components/CreateUserComponent';import ViewUserComponent from './components/ViewUserComponent';
function App() { return ( <div> <Router> <HeaderComponent /> <div className="container"> <Switch> <Route path = "/" exact component = {ListUserComponent}></Route><Route path = "/users" component = {ListUserComponent}></Route><Route path = "/add-user/:id" component = {CreateUserComponent}></Route><Route path = "/view-user/:id" component = {ViewUserComponent}></Route> </Switch> </div> <FooterComponent /> </Router> </div> );}
export default App;
import React from 'react';import './App.css';import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'import ListUserComponent from './components/ListUserComponent';import HeaderComponent from './components/HeaderComponent';import FooterComponent from './components/FooterComponent';import CreateUserComponent from './components/CreateUserComponent';import ViewUserComponent from './components/ViewUserComponent';
function App() { return ( <div> <Router> <HeaderComponent /> <div className="container"> <Switch> <Route path = "/" exact component = {ListUserComponent}></Route><Route path = "/users" component = {ListUserComponent}></Route><Route path = "/add-user/:id" component = {CreateUserComponent}></Route><Route path = "/view-user/:id" component = {ViewUserComponent}></Route> </Switch> </div> <FooterComponent /> </Router> </div> );}
export default App;
App.css
Path: /App.css.App { text-align: center;}
.App-logo { height: 40vmin; pointer-events: none;}
@media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; }}
.App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white;}
.App-link { color: #61dafb;}
@keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}
.footer { position: absolute; bottom: 0; width:100%; height: 50px; background-color: black; text-align: center; color: white;}
.App { text-align: center;}
.App-logo { height: 40vmin; pointer-events: none;}
@media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; }}
.App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white;}
.App-link { color: #61dafb;}
@keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}
.footer { position: absolute; bottom: 0; width:100%; height: 50px; background-color: black; text-align: center; color: white;}
index.js
Path: /index.jsimport React from 'react';import ReactDOM from 'react-dom';import './index.css';import App from './App';import * as serviceWorker from './serviceWorker';import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root'));
serviceWorker.unregister();
import React from 'react';import ReactDOM from 'react-dom';import './index.css';import App from './App';import * as serviceWorker from './serviceWorker';import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root'));
serviceWorker.unregister();
index.css
Path: /index.cssbody { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}
code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;}
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}
code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;}
Local Setup and Run the application
Download or clone the source code from GitHub to the local machine - Click here!
Backend
You can start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:9080.
OR
You can also start the application in debug mode in Visual Studio by opening the project root folder in Visual Studio and pressing F5 or by selecting Debug -> Start Debugging from the top menu, running in debug mode.
Frontend
npm install
npm start
Download or clone the source code from GitHub to the local machine - Click here!
Backend
You can start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:9080.
OR
You can also start the application in debug mode in Visual Studio by opening the project root folder in Visual Studio and pressing F5 or by selecting Debug -> Start Debugging from the top menu, running in debug mode.
Frontend
npm install
npm start