Hospital System with ASP.NET Core and React

Build a full-stack hospital management system with JWT auth and patient registration.

Introduction

30 Second Summary

Every hospital visit begins at the registration desk. A receptionist records your details, assigns a patient ID, and controls who can access your records.

In this project, you will build the foundation of a hospital management system for a hospital in Korutla. The system includes a secure backend API, a connected database, and a frontend where hospital staff can log in and register patients.

What You'll Build

A full-stack hospital management system with an ASP.NET Core 10 Web API backend, a PostgreSQL database, and a React + TypeScript frontend.

By the end of this project, you'll have:

  • An ASP.NET Core 10 Web API connected to PostgreSQL via Entity Framework Core, with a database schema for users, roles, and patients created through code-first migrations.
  • JWT-based authentication with four hospital roles (Admin, Doctor, Receptionist, Billing) and role-restricted API endpoints.
  • A React + TypeScript frontend with a login screen and a patient registration form that auto-generates unique patient IDs.
  • Secret Mission: Add patient search and filtering by name, patient ID, or patient type (OP/IP).

Are there any prerequisites?

This project assumes a basic understanding of programming concepts and databases. Your environment should already have Node.js v20.19.4, VS Code, and Ubuntu Linux set up.

Install Tools and Scaffold Both Projects

Building a hospital management system requires a backend API to handle data and authentication, and a frontend for hospital staff to interact with.

In this step, you will install the .NET 10 SDK and PostgreSQL, then scaffold two separate projects: an ASP.NET Core Web API for the backend and a React + TypeScript app for the frontend.

In this step, get ready to:
  • Install the .NET 10 SDK, PostgreSQL, and the C# Dev Kit VS Code extension.
  • Scaffold the ASP.NET Core Web API project with all required NuGet packages.
  • Scaffold the React + TypeScript frontend project with Vite.
Install .NET 10 SDK and PostgreSQL

The .NET 10 SDK provides the runtime and tooling needed to build and run ASP.NET Core applications. PostgreSQL is the relational database that will store all hospital data.

You will need a terminal for the commands that follow.

  • Press the Super key to open your search bar.
  • Type Terminal and press Enter to open it.
  • Install the .NET 10 SDK by running this command:
sudo apt-get update && sudo apt-get install -y dotnet-sdk-10.0
  • Verify the installation succeeded by running:
dotnet --version

✔️ I see a version starting with 10.0

The .NET 10 SDK is installed and ready to go.

ⓧ Command not found or an error

The SDK may not have installed correctly.

  • Install PostgreSQL and start the database service by running these commands:
sudo apt install -y postgresql
sudo systemctl start postgresql
  • Verify PostgreSQL is running by running:
sudo systemctl status postgresql

You should see active (exited) or active (running) in the output. Press q to exit the status view.

PostgreSQL failed to start?

If you see an error about the service not being found, double check that the install command finished without errors. Run sudo apt install -y postgresql again to retry.

Help me troubleshoot my PostgreSQL installation on Ubuntu.

Now you need to create a database and a database user for the hospital system.

  • Open the PostgreSQL command-line tool by running:
sudo -u postgres psql
  • Inside the PostgreSQL prompt, create the database user, database, and permissions by running these commands one at a time:
CREATE USER hospitaladmin WITH PASSWORD 'hospital123';
CREATE DATABASE korutla_hospital OWNER hospitaladmin;
GRANT ALL PRIVILEGES ON DATABASE korutla_hospital TO hospitaladmin;
\q

What do these commands do?

  • CREATE USER creates a new PostgreSQL user called hospitaladmin with the password hospital123.
  • CREATE DATABASE creates the korutla_hospital database and assigns hospitaladmin as its owner.
  • GRANT ALL PRIVILEGES gives the hospitaladmin user full access to the database.
  • \q exits the PostgreSQL prompt and returns you to the regular terminal.

If all four commands ran without errors, your database is ready.

Seeing a permission error?

Make sure you entered the PostgreSQL prompt using sudo -u postgres psql. Running psql on its own will try to connect as your Linux user, which does not have PostgreSQL permissions.

Help me fix PostgreSQL permission errors on Ubuntu.

The last tool to install is the C# Dev Kit extension for VS Code, which provides IntelliSense and debugging support for C# projects.

  • Switch to VS Code.
  • Press Ctrl+Shift+X to open the Extensions sidebar.
  • Type C# Dev Kit into the search bar.
  • Click Install on the C# Dev Kit extension by Microsoft.

Once the extension finishes installing, you should see a green checkmark or Installed label next to the extension name.

Scaffold the ASP.NET Core Web API project

The backend will be an ASP.NET Core Web API that uses controllers to handle HTTP requests. You will scaffold the project, then add packages for PostgreSQL, authentication, and database migrations.

  • Switch back to your terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder and move into it by running these commands:
mkdir korutla-hospital
cd korutla-hospital
  • Confirm the folder was created by running:
pwd

You should see a path ending in korutla-hospital.

  • Scaffold the ASP.NET Core Web API project by running:
dotnet new webapi --use-controllers -o HospitalApi

What does this command do?

  • dotnet new webapi generates a new Web API project from the built-in template.
  • --use-controllers tells the template to use controller-based routing instead of minimal APIs. Controllers give each resource its own file, which makes the project easier to organize.
  • -o HospitalApi places all the generated files inside a new HospitalApi folder.
  • Verify the scaffold created the folder by running:
ls

You should see HospitalApi listed in the output.

  • Move into the HospitalApi folder and add the four NuGet packages the project needs by running these commands:
cd HospitalApi
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 10.0.2
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 10.0.9
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 10.0.9
dotnet add package Microsoft.EntityFrameworkCore.Design --version 10.0.9

What does each package do?

  • Npgsql.EntityFrameworkCore.PostgreSQL connects Entity Framework Core to PostgreSQL so you can query the database using C# instead of raw SQL.
  • Microsoft.AspNetCore.Authentication.JwtBearer enables JWT authentication, which is how the API will verify staff logins.
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore provides built-in user and role management tables and logic.
  • Microsoft.EntityFrameworkCore.Design is required for running database migration commands from the terminal.

Each command will download the package and print a confirmation. If you see any errors about a package not being found, double check the package name and version number match exactly.

  • Install the Entity Framework Core command-line tool globally by running:
dotnet tool install --global dotnet-ef

Seeing a PATH warning?

On Ubuntu, the dotnet-ef tool installs to ~/.dotnet/tools/. If your terminal cannot find the command after install, add the tools directory to your PATH by running:

export PATH="$PATH:$HOME/.dotnet/tools/"

To make this permanent, add that same line to the end of your ~/.bashrc file, then close and reopen your terminal.

Help me fix the dotnet-ef PATH issue on Ubuntu.

  • Verify the entire API project builds successfully by running:
dotnet build

✔️ I see Build succeeded

Your API project is scaffolded and all packages are installed correctly.

ⓧ I see errors

  • Make sure you ran all four dotnet add package commands and each one completed without errors.
  • Confirm you are inside the HospitalApi folder by running pwd. The path should end with korutla-hospital/HospitalApi.

Help me troubleshoot my dotnet build errors.

Scaffold the React frontend

The frontend will be a React app built with Vite, a fast build tool for modern web projects. Hospital staff will use this app to log in and register patients.

  • Navigate back to the korutla-hospital root directory by running:
cd ..
  • Scaffold the React + TypeScript frontend project by running:
npm create vite@latest hospital-frontend -- --template react-ts

What does this command do?

  • npm create vite@latest runs the Vite project scaffolding tool.
  • hospital-frontend is the name of the new folder Vite will create.
  • --template react-ts tells Vite to set up the project with React and TypeScript preconfigured.

Allow the package install

If you see a prompt asking to install the create-vite package, type y and press Enter to proceed.

  • Verify the frontend folder was created by running:
ls

You should see both HospitalApi and hospital-frontend listed in the output.

  • Move into the frontend folder and install the project dependencies by running these commands:
cd hospital-frontend
npm install
  • Install Axios for making API calls from the frontend by running:
npm install axios

Why Axios instead of fetch?

Axios provides built-in support for request interceptors. Later in the project, you will use an interceptor to automatically attach the JWT authentication token to every API request. This saves you from manually adding the token to each call.

  • Start the Vite development server to verify everything works by running:
npm run dev

You should see output with a URL like http://localhost:5173. This confirms the React frontend is running.

  • Press Ctrl+C in the terminal to stop the development server.

Not seeing the localhost URL?

  • Make sure you ran npm install before npm run dev. The dev server needs the installed packages to start.
  • Confirm you are inside the hospital-frontend folder by running pwd.

Help me get my Vite React dev server running.

Both projects are scaffolded and verified. Next up, you will design the database schema for users, roles, and patients, then run your first Entity Framework Core migration to create the tables.

Design the Database Schema and Run Migrations

Your API project is scaffolded and your PostgreSQL database is waiting. Before the hospital system can store any data, you need to define what that data looks like in code.

In this step, you will create C# entity classes that represent database tables, wire them up to PostgreSQL through a database context, and run your first Entity Framework Core migration to generate the actual tables.

In this step, get ready to:
  • Create the Patient entity model and database context.
  • Configure PostgreSQL, Identity, and JWT authentication in Program.cs.
  • Run the initial database migration to create all tables.
Create the Entity Framework Core DbContext and Patient entity model

The hospital system needs to track patients. In Entity Framework Core, you define database tables as C# classes called entities. The framework reads these classes and creates matching tables in your database.

  • In VS Code, open the HospitalApi project folder inside korutla-hospital.
  • Create a new folder called Models inside HospitalApi.
  • Create a new file called PatientType.cs inside the Models folder, then paste the following code:
namespace HospitalApi.Models;

// Defines whether a patient is outpatient or inpatient
public enum PatientType
{
    Outpatient,
    Inpatient
}

What does this code do?

This enum defines the two patient categories your hospital handles. Outpatient means the patient visits for treatment and leaves the same day. Inpatient means the patient is admitted and stays overnight.

Entity Framework Core stores this enum as a string in the database, so the Patients table will have a column showing either "Outpatient" or "Inpatient".

You won't see a change yet. The next file uses this enum.

  • Create another new file called Patient.cs inside the Models folder, then paste the following code:
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.Models;

// Represents a patient record in the hospital database
public class Patient
{
    public int Id { get; set; }

    [Required]
    [MaxLength(20)]
    public string PatientUniqueId { get; set; } = string.Empty;

    [Required]
    [MaxLength(100)]
    public string FirstName { get; set; } = string.Empty;

    [Required]
    [MaxLength(100)]
    public string LastName { get; set; } = string.Empty;

    public DateOnly DateOfBirth { get; set; }

    [Required]
    [MaxLength(10)]
    public string Gender { get; set; } = string.Empty;

    [Required]
    [MaxLength(15)]
    public string PhoneNumber { get; set; } = string.Empty;

    [MaxLength(500)]
    public string Address { get; set; } = string.Empty;

    // Uses the PatientType enum to distinguish OP/IP
    public PatientType PatientType { get; set; }

    public DateTime RegisteredAt { get; set; } = DateTime.UtcNow;

    // Foreign key linking to the Identity user who registered this patient
    [Required]
    public string RegisteredById { get; set; } = string.Empty;
}

What does this code do?

Each property in this class becomes a column in the Patients database table. The [Required] and [MaxLength] attributes tell EF Core to enforce constraints at the database level.

PatientUniqueId holds the auto-generated hospital ID (like KRL-260001). RegisteredById links each patient to the staff member who registered them.

  • Save both PatientType.cs and Patient.cs.

You won't see a visible change yet. These models define the shape of the data, but the database context in the next chunk connects them to PostgreSQL.

Seeing red squiggles in Patient.cs?

Make sure PatientType.cs is saved and sits in the same Models folder. Both files share the HospitalApi.Models namespace, so the Patient class can reference the PatientType enum.

Help me debug red squiggles in my Patient.cs entity model.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is PatientType.cs:

namespace HospitalApi.Models;

public enum PatientType
{
    Outpatient,
    Inpatient
}

Here is Patient.cs:

using System.ComponentModel.DataAnnotations;

namespace HospitalApi.Models;

public class Patient
{
    public int Id { get; set; }

    [Required]
    [MaxLength(20)]
    public string PatientUniqueId { get; set; } = string.Empty;

    [Required]
    [MaxLength(100)]
    public string FirstName { get; set; } = string.Empty;

    [Required]
    [MaxLength(100)]
    public string LastName { get; set; } = string.Empty;

    public DateOnly DateOfBirth { get; set; }

    [Required]
    [MaxLength(10)]
    public string Gender { get; set; } = string.Empty;

    [Required]
    [MaxLength(15)]
    public string PhoneNumber { get; set; } = string.Empty;

    [MaxLength(500)]
    public string Address { get; set; } = string.Empty;

    public PatientType PatientType { get; set; }

    public DateTime RegisteredAt { get; set; } = DateTime.UtcNow;

    [Required]
    public string RegisteredById { get; set; } = string.Empty;
}
  • Create a new folder called Data inside HospitalApi.
  • Create a new file called HospitalDbContext.cs inside the Data folder, then paste the following code:
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using HospitalApi.Models;

namespace HospitalApi.Data;

// Database context that combines Identity tables with hospital-specific tables
public class HospitalDbContext : IdentityDbContext<IdentityUser>
{
    public HospitalDbContext(DbContextOptions<HospitalDbContext> options) : base(options) { }

    // Adds a Patients table alongside the Identity tables
    public DbSet<Patient> Patients => Set<Patient>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        // Let Identity configure its own tables first
        base.OnModelCreating(builder);

        builder.Entity<Patient>(entity =>
        {
            // Ensure no two patients share the same unique ID
            entity.HasIndex(p => p.PatientUniqueId).IsUnique();
            // Store the enum as a readable string instead of an integer
            entity.Property(p => p.PatientType)
                  .HasConversion<string>();
        });
    }
}

What does this code do?

HospitalDbContext extends IdentityDbContext, which means it inherits all the built-in ASP.NET Core Identity tables for users, roles, and authentication. On top of those, it adds a Patients table.

The OnModelCreating method applies two customizations. It creates a unique index on PatientUniqueId so no duplicate hospital IDs can exist. It also converts the PatientType enum to a string, making the database easier to read.

  • Save HospitalDbContext.cs.

You won't see a change yet. The next substep connects this context to PostgreSQL and configures authentication.

Seeing errors about missing namespaces?

Make sure the Models folder contains both PatientType.cs and Patient.cs, and that all three files use the correct namespace (HospitalApi.Models for the models, HospitalApi.Data for the context).

Help me fix namespace or missing type errors in my HospitalDbContext.

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using HospitalApi.Models;

namespace HospitalApi.Data;

public class HospitalDbContext : IdentityDbContext<IdentityUser>
{
    public HospitalDbContext(DbContextOptions<HospitalDbContext> options) : base(options) { }

    public DbSet<Patient> Patients => Set<Patient>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<Patient>(entity =>
        {
            entity.HasIndex(p => p.PatientUniqueId).IsUnique();
            entity.Property(p => p.PatientType)
                  .HasConversion<string>();
        });
    }
}
Configure PostgreSQL connection and Identity in Program.cs

The models and context define the data structure. Now you need to tell the application how to connect to PostgreSQL, enable user authentication, and allow the React frontend to communicate with the API.

  • Open appsettings.json in the HospitalApi folder.
  • Replace the entire file contents with the following configuration:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=korutla_hospital;Username=hospitaladmin;Password=hospital123"
  },
  "Jwt": {
    "Key": "KorutlaHospitalSuperSecretKey2026!MustBe32CharsLong",
    "Issuer": "KorutlaHospitalApi",
    "Audience": "KorutlaHospitalFrontend"
  }
}

What does this configuration do?

The ConnectionStrings.DefaultConnection tells Entity Framework Core how to connect to the korutla_hospital PostgreSQL database you created in Step 1.

The Jwt section stores the signing key, issuer, and audience for JWT token generation. The key must be at least 32 characters for HMAC-SHA256 signing.

  • Save appsettings.json.

You won't see a visible change yet. The next chunk reads these values in Program.cs.

  • Open Program.cs in the HospitalApi folder.
  • Replace the entire file contents with the following code:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using HospitalApi.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOpenApi();

// Register the database context with PostgreSQL
builder.Services.AddDbContext<HospitalDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

// Configure ASP.NET Core Identity with password rules
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<HospitalDbContext>()
.AddDefaultTokenProviders();

// Read JWT settings from appsettings.json
var jwtKey = builder.Configuration["Jwt:Key"]!;
var jwtIssuer = builder.Configuration["Jwt:Issuer"]!;
var jwtAudience = builder.Configuration["Jwt:Audience"]!;

// Configure JWT-based authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = jwtIssuer,
        ValidAudience = jwtAudience,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
    };
});

// Allow the React frontend to make cross-origin requests
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("http://localhost:5173")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

// Middleware order matters: CORS, then auth, then controllers
app.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

What does this code do?

This file configures four key services before the application starts:

  • AddDbContext registers HospitalDbContext with the PostgreSQL connection string from appsettings.json.
  • AddIdentity sets up ASP.NET Core Identity for user and role management, storing data in your HospitalDbContext.
  • AddJwtBearer configures JWT token validation so the API can verify tokens on every request.
  • AddCors allows the React dev server at http://localhost:5173 to call the API without being blocked by the browser.

Why is CORS needed?

CORS stands for Cross-Origin Resource Sharing. Browsers block requests between different origins by default. Since the React frontend runs on port 5173 and the API runs on a different port, the API must explicitly allow that origin.

  • Save Program.cs.
  • Verify the project compiles by running this command in your terminal (make sure you are inside the HospitalApi folder):
dotnet build
  • You should see Build succeeded with 0 errors in the output.

Build failed with errors?

Check that the Data and Models folders are inside the HospitalApi project directory, not in the parent korutla-hospital folder. Each file's namespace must match its folder path.

If you see an error about a missing using directive, double-check that the import statements at the top of each file match the code above exactly.

Help me fix build errors in my HospitalApi project after adding the DbContext and Program.cs changes.

✔️ Awesome, I've got everything!

Great. Make sure you saw Build succeeded before continuing.

ⓧ I'd like to double check the full code

Here is appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=korutla_hospital;Username=hospitaladmin;Password=hospital123"
  },
  "Jwt": {
    "Key": "KorutlaHospitalSuperSecretKey2026!MustBe32CharsLong",
    "Issuer": "KorutlaHospitalApi",
    "Audience": "KorutlaHospitalFrontend"
  }
}

Here is Program.cs:

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using HospitalApi.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOpenApi();

builder.Services.AddDbContext<HospitalDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<HospitalDbContext>()
.AddDefaultTokenProviders();

var jwtKey = builder.Configuration["Jwt:Key"]!;
var jwtIssuer = builder.Configuration["Jwt:Issuer"]!;
var jwtAudience = builder.Configuration["Jwt:Audience"]!;

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = jwtIssuer,
        ValidAudience = jwtAudience,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
    };
});

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("http://localhost:5173")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();
Create and apply the initial migration

Your C# models and database context are ready. Now EF Core needs to translate them into actual PostgreSQL tables. This is done through a migration, which generates SQL commands to create the tables.

  • Generate the migration by running this command in your terminal (inside the HospitalApi folder):
dotnet ef migrations add InitialCreate

What does this command do?

This command reads your HospitalDbContext and entity models, then generates C# migration files inside a new Migrations folder. These files contain Up and Down methods that create or drop the database tables.

  • Confirm the migration files were created by running:
ls Migrations
  • You should see files like _InitialCreate.cs and HospitalDbContextModelSnapshot.cs in the output.

Command not found or build errors?

If you see a "command not found" error for dotnet ef, make sure the EF Core CLI tool is installed. Run dotnet tool install --global dotnet-ef and try again. You may also need to restart your terminal or add ~/.dotnet/tools to your PATH.

If the command fails with a build error, run dotnet build first and fix any compilation issues before retrying the migration.

Help me fix dotnet ef migrations add errors in my HospitalApi project.

  • Apply the migration to your PostgreSQL database by running:
dotnet ef database update
  • You should see log output ending with Done. in your terminal.
  • Verify the tables were created by connecting to PostgreSQL and listing them:
sudo -u postgres psql -d korutla_hospital -c "\dt"
  • You should see a list of tables including AspNetUsers, AspNetRoles, AspNetUserRoles, and Patients in the output.

Where did all these tables come from?

You only defined one entity (Patient), but the migration created eight tables. The seven AspNet* tables come from IdentityDbContext. ASP.NET Core Identity automatically manages tables for users, roles, claims, logins, and tokens.

Your Patients table sits alongside these Identity tables in the same database, all managed through a single HospitalDbContext.

Database update failed?

If you see a connection error, make sure PostgreSQL is running. Run sudo systemctl start postgresql and try again.

If you see a permission error, verify that the hospitaladmin user has the correct password (hospital123) and owns the korutla_hospital database.

Help me fix dotnet ef database update connection or permission errors.

Your database schema is in place. Next up, you will build the authentication system with JWT tokens and seed the four hospital roles.

Build Authentication with JWT and Role Seeding

Your database schema is in place with Identity tables and a Patients table. Now you need to control who can access the system and what they can do once they're in.

In this step, you will seed the database with four hospital roles and a default admin account, then build API endpoints that issue JWT tokens when staff log in. These tokens carry the user's role, so the API can enforce who is allowed to register patients, view records, or manage other staff.

In this step, get ready to:
  • Seed four hospital roles (Admin, Doctor, Receptionist, Billing) and a default admin user into the database.
  • Build login and registration API endpoints that issue and validate JWT tokens.
  • Test the login endpoint and verify the returned token contains the correct role claim.
Create a data seeder for roles and a default admin user

A hospital system needs predefined roles so that different staff members get the right level of access. Instead of manually inserting roles into the database, you can write a seeder that runs automatically every time the API starts.

What is seeding?

Seeding is the process of populating a database with initial data that the application needs to function. In this case, the seeder creates four roles and one admin account so you can log in immediately after the first launch.

Your Program.cs already calls DbSeeder.SeedAsync after app.Build(). The seeder class itself does not exist yet, so let's create it now.

  • In VS Code, open the Data/ folder inside HospitalApi.
  • Create a new file called DbSeeder.cs and paste the following code:
using Microsoft.AspNetCore.Identity;

namespace HospitalApi.Data;

public static class DbSeeder
{
    public static async Task SeedAsync(IServiceProvider serviceProvider)
    {
        // Get Identity managers from the DI container
        var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
        var userManager = serviceProvider.GetRequiredService<UserManager<IdentityUser>>();

        // Define the four hospital roles
        string[] roles = { "Admin", "Doctor", "Receptionist", "Billing" };

        // Create each role if it doesn't already exist
        foreach (var role in roles)
        {
            if (!await roleManager.RoleExistsAsync(role))
            {
                await roleManager.CreateAsync(new IdentityRole(role));
            }
        }

        // Create a default admin user for first-time login
        var adminEmail = "admin@korutla.hospital";
        var adminUser = await userManager.FindByEmailAsync(adminEmail);

        if (adminUser == null)
        {
            adminUser = new IdentityUser
            {
                UserName = adminEmail,
                Email = adminEmail,
                EmailConfirmed = true
            };

            var result = await userManager.CreateAsync(adminUser, "Admin@123");

            if (result.Succeeded)
            {
                await userManager.AddToRoleAsync(adminUser, "Admin");
            }
        }
    }
}

What does this code do?

The seeder uses ASP.NET Core Identity's built-in managers to interact with the database. RoleManager creates roles, and UserManager creates user accounts.

Every check uses an "if not exists" pattern so the seeder is safe to run repeatedly. The first launch inserts the data. Every launch after that skips the inserts because the roles and admin user are already present.

  • Save DbSeeder.cs.
  • Confirm the project compiles by running this in your terminal (make sure you are inside the HospitalApi folder):
dotnet build
  • You should see Build succeeded with 0 errors in the output.

Build failed?

If you see a namespace or type error mentioning DbSeeder, double-check that the file is saved inside the Data/ folder and that the namespace is HospitalApi.Data. The namespace must match what Program.cs expects.

help me fix a build error in my DbSeeder.cs file

Create the AuthController with Register and Login endpoints

Before building the controller, you need Data Transfer Objects (DTOs) that define the shape of the data coming into and going out of the auth endpoints. DTOs keep your API contract clean by exposing only the fields the client needs.

  • Create a new folder called DTOs inside HospitalApi.
  • Create a file called LoginDto.cs inside the DTOs folder and paste the following code:
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.DTOs;

// Incoming data for the login endpoint
public class LoginDto
{
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Required]
    public string Password { get; set; } = string.Empty;
}
  • Create a file called RegisterDto.cs in the same DTOs folder and paste the following code:
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.DTOs;

// Incoming data for the register endpoint (admin-only)
public class RegisterDto
{
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Required]
    [MinLength(6)]
    public string Password { get; set; } = string.Empty;

    [Required]
    public string Role { get; set; } = string.Empty;
}
  • Create a file called AuthResponseDto.cs in the same DTOs folder and paste the following code:
namespace HospitalApi.DTOs;

// Data returned after a successful login
public class AuthResponseDto
{
    public string Token { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Role { get; set; } = string.Empty;
}

What do these DTOs do?

  • LoginDto defines what the client sends to log in: an email and a password.
  • RegisterDto adds a Role field so the admin can assign a role when creating a new staff account.
  • AuthResponseDto defines what the API sends back after a successful login: the JWT token, the user's email, and their role.
  • Save all three DTO files.

You won't see a change yet. The DTOs are used by the controller you will create next.

  • Open the Controllers/ folder inside HospitalApi.
  • Create a new file called AuthController.cs and paste the following code:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using HospitalApi.DTOs;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly IConfiguration _configuration;

    public AuthController(
        UserManager<IdentityUser> userManager,
        RoleManager<IdentityRole> roleManager,
        IConfiguration configuration)
    {
        _userManager = userManager;
        _roleManager = roleManager;
        _configuration = configuration;
    }

    // POST /api/auth/login - Validates credentials and returns a JWT token
    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginDto dto)
    {
        var user = await _userManager.FindByEmailAsync(dto.Email);
        if (user == null || !await _userManager.CheckPasswordAsync(user, dto.Password))
        {
            return Unauthorized(new { message = "Invalid email or password." });
        }

        var roles = await _userManager.GetRolesAsync(user);
        var token = GenerateJwtToken(user, roles);

        return Ok(new AuthResponseDto
        {
            Token = token,
            Email = user.Email!,
            Role = roles.FirstOrDefault() ?? ""
        });
    }

    // POST /api/auth/register - Admin-only endpoint to create new staff users
    [HttpPost("register")]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> Register([FromBody] RegisterDto dto)
    {
        if (!await _roleManager.RoleExistsAsync(dto.Role))
        {
            return BadRequest(new { message = $"Role '{dto.Role}' does not exist." });
        }

        var user = new IdentityUser
        {
            UserName = dto.Email,
            Email = dto.Email,
            EmailConfirmed = true
        };

        var result = await _userManager.CreateAsync(user, dto.Password);

        if (!result.Succeeded)
        {
            return BadRequest(new { errors = result.Errors.Select(e => e.Description) });
        }

        await _userManager.AddToRoleAsync(user, dto.Role);

        return Ok(new { message = $"User '{dto.Email}' created with role '{dto.Role}'." });
    }

    // Builds a signed JWT containing the user's ID, email, and roles
    private string GenerateJwtToken(IdentityUser user, IList<string> roles)
    {
        var jwtKey = _configuration["Jwt:Key"]!;
        var jwtIssuer = _configuration["Jwt:Issuer"]!;
        var jwtAudience = _configuration["Jwt:Audience"]!;

        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id),
            new(ClaimTypes.Email, user.Email!),
        };

        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: jwtIssuer,
            audience: jwtAudience,
            claims: claims,
            expires: DateTime.UtcNow.AddHours(8),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

What does this controller do?

  • Login looks up the user by email, checks the password, and calls GenerateJwtToken to create a signed token containing the user's role. The token is returned along with the email and role in an AuthResponseDto.
  • Register is protected by the [Authorize(Roles = "Admin")] attribute. Only a logged-in admin can create new staff users and assign them a role.
  • GenerateJwtToken reads the JWT key, issuer, and audience from appsettings.json, packs the user's ID, email, and roles into claims, and signs the token with HMAC-SHA256. The token expires after 8 hours.

What is role-based access control?

Role-based access control (RBAC) restricts actions based on a user's assigned role. In this system, a Receptionist can register patients but cannot create new staff accounts. An Admin can do both. The [Authorize(Roles = "...")] attribute on each endpoint enforces these rules automatically.

  • Save AuthController.cs.
  • Confirm everything compiles by running:
dotnet build
  • You should see Build succeeded with 0 errors.

Seeing a build error?

  • If you see an error about LoginDto, RegisterDto, or AuthResponseDto not being found, verify that your three DTO files are saved inside the DTOs/ folder and that each file's namespace is HospitalApi.DTOs.
  • If the error mentions JwtSecurityToken or SymmetricSecurityKey, make sure you added the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package in Step 1.

help me fix a build error in my AuthController

✔️ Awesome, I've got everything!

Great. Double check you have saved all five new files before moving on.

ⓧ I'd like to double check the full code

Here is the complete AuthController.cs file for comparison:

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using HospitalApi.DTOs;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly IConfiguration _configuration;

    public AuthController(
        UserManager<IdentityUser> userManager,
        RoleManager<IdentityRole> roleManager,
        IConfiguration configuration)
    {
        _userManager = userManager;
        _roleManager = roleManager;
        _configuration = configuration;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginDto dto)
    {
        var user = await _userManager.FindByEmailAsync(dto.Email);
        if (user == null || !await _userManager.CheckPasswordAsync(user, dto.Password))
        {
            return Unauthorized(new { message = "Invalid email or password." });
        }

        var roles = await _userManager.GetRolesAsync(user);
        var token = GenerateJwtToken(user, roles);

        return Ok(new AuthResponseDto
        {
            Token = token,
            Email = user.Email!,
            Role = roles.FirstOrDefault() ?? ""
        });
    }

    [HttpPost("register")]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> Register([FromBody] RegisterDto dto)
    {
        if (!await _roleManager.RoleExistsAsync(dto.Role))
        {
            return BadRequest(new { message = $"Role '{dto.Role}' does not exist." });
        }

        var user = new IdentityUser
        {
            UserName = dto.Email,
            Email = dto.Email,
            EmailConfirmed = true
        };

        var result = await _userManager.CreateAsync(user, dto.Password);

        if (!result.Succeeded)
        {
            return BadRequest(new { errors = result.Errors.Select(e => e.Description) });
        }

        await _userManager.AddToRoleAsync(user, dto.Role);

        return Ok(new { message = $"User '{dto.Email}' created with role '{dto.Role}'." });
    }

    private string GenerateJwtToken(IdentityUser user, IList<string> roles)
    {
        var jwtKey = _configuration["Jwt:Key"]!;
        var jwtIssuer = _configuration["Jwt:Issuer"]!;
        var jwtAudience = _configuration["Jwt:Audience"]!;

        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id),
            new(ClaimTypes.Email, user.Email!),
        };

        foreach (var role in roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: jwtIssuer,
            audience: jwtAudience,
            claims: claims,
            expires: DateTime.UtcNow.AddHours(8),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
Test authentication

The seeder, DTOs, and controller are all in place. Time to start the API and confirm the login flow works end to end.

  • Start the API by running:
dotnet run
  • Watch the terminal output. You should see a line that says Now listening on: http://localhost: followed by a port number.
  • Note the port number. The project uses 5057 in the examples below, but yours may differ. Replace it if needed.

What happened when the API started?

Behind the scenes, DbSeeder.SeedAsync ran and inserted the four roles (Admin, Doctor, Receptionist, Billing) into the AspNetRoles table. It also created the admin user (admin@korutla.hospital) and assigned it the Admin role.

  • Open a second terminal window. Test the login endpoint with the seeded admin credentials by running:
curl -X POST http://localhost:5057/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@korutla.hospital","password":"Admin@123"}'
  • You should see a JSON response containing a token, email, and role field. The role value should be Admin.

Getting a connection refused error?

  • Make sure dotnet run is still running in your first terminal. The API must be running for the request to succeed.
  • Check that the port in your curl command matches the port shown in the terminal output. If your API started on a different port, update the command.
  • If you see an "Unauthorized" response, double-check that the email is admin@korutla.hospital and the password is Admin@123 (case-sensitive).

help me debug my login API request

Now verify that the JWT token actually contains the Admin role claim. Copy the long token string from the response (everything between the quotes).

  • Decode the token's payload by running this command, replacing the placeholder with your actual token:
echo [[JWT_TOKEN="your token string from the login response"]] | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null
  • You should see JSON output containing a role field with the value Admin. This confirms the token carries the correct role claim.

What is a JWT token?

A JWT (JSON Web Token) is a compact, self-contained string split into three parts separated by dots: a header, a payload, and a signature. The payload contains claims like the user's ID, email, and role.

The API signs the token with the secret key from appsettings.json. When the client sends the token back on future requests, the API verifies the signature and reads the role claim to decide what the user is allowed to do.

You can also decode tokens at jwt.io

Paste your token at jwt.io to see a formatted breakdown of the header, payload, and signature. This is handy for quick debugging during development.

Authentication is locked in. Your API can verify staff credentials, issue role-aware tokens, and restrict endpoints to specific roles. Next up, you will build the patient registration endpoints that put these roles to work.

Build the Patient Registration API

Your authentication system is live. Staff can log in with role-based JWT tokens, and only admins can register new users.

Now it's time to build the core feature every hospital needs: patient registration. In this step, you'll create API endpoints that let authorized staff register outpatient and inpatient records, each with an auto-generated unique patient ID.

In this step, get ready to:
  • Create DTOs for patient registration requests and responses.
  • Build a PatientsController with create and read endpoints.
  • Test the endpoints using curl with your JWT token.
Create the patient registration DTOs

Before building the controller, you need data transfer objects (DTOs) to define the shape of incoming registration requests and outgoing patient responses. DTOs keep your API contract separate from your database entities.

What is a DTO?

A DTO (Data Transfer Object) is a simple class that defines what data crosses an API boundary. Your Patient entity has fields like RegisteredById and PatientUniqueId that the server generates. The client should never send those.

A CreatePatientDto accepts only the fields the client provides. A PatientResponseDto returns only the fields the client should see.

  • Create a new file called CreatePatientDto.cs inside the DTOs/ folder in your HospitalApi project.
  • Paste the following code into the file:
using System.ComponentModel.DataAnnotations;
using HospitalApi.Models;

namespace HospitalApi.DTOs;

public class CreatePatientDto
{
    [Required]
    [MaxLength(100)]
    public string FirstName { get; set; } = string.Empty;

    [Required]
    [MaxLength(100)]
    public string LastName { get; set; } = string.Empty;

    public DateOnly DateOfBirth { get; set; }

    [Required]
    [MaxLength(10)]
    public string Gender { get; set; } = string.Empty;

    [Required]
    [MaxLength(15)]
    public string PhoneNumber { get; set; } = string.Empty;

    [MaxLength(500)]
    public string Address { get; set; } = string.Empty;

    // Uses the PatientType enum (Outpatient or Inpatient)
    public PatientType PatientType { get; set; }
}

What does this code do?

This DTO defines the fields a client sends when registering a new patient. The [Required] and [MaxLength] attributes enforce validation automatically. If a required field is missing, ASP.NET Core returns a 400 Bad Request before your controller code even runs.

Notice that server-generated fields like PatientUniqueId, RegisteredAt, and RegisteredById are intentionally absent. The server will fill those in.

  • Save CreatePatientDto.cs.
  • You won't see a visible change yet. The next file and controller will use this DTO.
  • Create another new file called PatientResponseDto.cs inside the DTOs/ folder.
  • Paste the following code into the file:
using HospitalApi.Models;

namespace HospitalApi.DTOs;

public class PatientResponseDto
{
    public int Id { get; set; }

    // The auto-generated unique ID (e.g., KRL-260001)
    public string PatientUniqueId { get; set; } = string.Empty;

    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public DateOnly DateOfBirth { get; set; }
    public string Gender { get; set; } = string.Empty;
    public string PhoneNumber { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;

    // Outpatient or Inpatient
    public PatientType PatientType { get; set; }

    public DateTime RegisteredAt { get; set; }
}

What does this code do?

This DTO defines the shape of patient data returned to the client. It includes PatientUniqueId and RegisteredAt so the client can see the generated ID and timestamp. It excludes RegisteredById because that's an internal database reference the client doesn't need.

  • Save PatientResponseDto.cs.
  • You won't see a visible change yet. The controller in the next substep uses both DTOs.

Seeing red squiggles in the DTO files?

Make sure the namespace in each file is HospitalApi.DTOs (matching the folder path). Also confirm your file is saved inside the DTOs/ folder, not the project root.

help me fix namespace errors in my DTO files

✔️ Awesome, I've got everything!

Great. Make sure both DTO files are saved before moving on.

ⓧ I'd like to double check the full code

Here is CreatePatientDto.cs:

using System.ComponentModel.DataAnnotations;
using HospitalApi.Models;

namespace HospitalApi.DTOs;

public class CreatePatientDto
{
    [Required]
    [MaxLength(100)]
    public string FirstName { get; set; } = string.Empty;

    [Required]
    [MaxLength(100)]
    public string LastName { get; set; } = string.Empty;

    public DateOnly DateOfBirth { get; set; }

    [Required]
    [MaxLength(10)]
    public string Gender { get; set; } = string.Empty;

    [Required]
    [MaxLength(15)]
    public string PhoneNumber { get; set; } = string.Empty;

    [MaxLength(500)]
    public string Address { get; set; } = string.Empty;

    public PatientType PatientType { get; set; }
}

Here is PatientResponseDto.cs:

using HospitalApi.Models;

namespace HospitalApi.DTOs;

public class PatientResponseDto
{
    public int Id { get; set; }
    public string PatientUniqueId { get; set; } = string.Empty;
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public DateOnly DateOfBirth { get; set; }
    public string Gender { get; set; } = string.Empty;
    public string PhoneNumber { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public PatientType PatientType { get; set; }
    public DateTime RegisteredAt { get; set; }
}
Create the PatientsController with CRUD endpoints

The controller is where the API logic lives. It handles incoming HTTP requests, interacts with the database through Entity Framework Core, and returns responses using the DTOs you just created.

  • Create a new file called PatientsController.cs inside the Controllers/ folder in your HospitalApi project.
  • Paste the following code into the file:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HospitalApi.Data;
using HospitalApi.DTOs;
using HospitalApi.Models;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class PatientsController : ControllerBase
{
    private readonly HospitalDbContext _context;

    public PatientsController(HospitalDbContext context)
    {
        _context = context;
    }

    // POST /api/patients - Only Admin and Receptionist can register patients
    [HttpPost]
    [Authorize(Roles = "Admin,Receptionist")]
    public async Task<IActionResult> CreatePatient([FromBody] CreatePatientDto dto)
    {
        // Get the logged-in user's ID from the JWT token claims
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
        var uniqueId = await GeneratePatientUniqueId();

        var patient = new Patient
        {
            PatientUniqueId = uniqueId,
            FirstName = dto.FirstName,
            LastName = dto.LastName,
            DateOfBirth = dto.DateOfBirth,
            Gender = dto.Gender,
            PhoneNumber = dto.PhoneNumber,
            Address = dto.Address,
            PatientType = dto.PatientType,
            RegisteredAt = DateTime.UtcNow,
            RegisteredById = userId
        };

        _context.Patients.Add(patient);
        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetPatient), new { id = patient.Id }, MapToResponse(patient));
    }

    // GET /api/patients - Any authenticated user can view the patient list
    [HttpGet]
    public async Task<IActionResult> GetPatients()
    {
        var patients = await _context.Patients
            .OrderByDescending(p => p.RegisteredAt)
            .Select(p => MapToResponse(p))
            .ToListAsync();

        return Ok(patients);
    }

    // GET /api/patients/{id} - Any authenticated user can view a single patient
    [HttpGet("{id}")]
    public async Task<IActionResult> GetPatient(int id)
    {
        var patient = await _context.Patients.FindAsync(id);

        if (patient == null)
        {
            return NotFound();
        }

        return Ok(MapToResponse(patient));
    }

    // Generates a unique ID like KRL-260001 (KRL-YYNNNN)
    private async Task<string> GeneratePatientUniqueId()
    {
        // Get the last 2 digits of the current year
        var year = DateTime.UtcNow.Year % 100;
        var prefix = $"KRL-{year:D2}";

        // Find the last patient registered this year
        var lastPatient = await _context.Patients
            .Where(p => p.PatientUniqueId.StartsWith(prefix))
            .OrderByDescending(p => p.PatientUniqueId)
            .FirstOrDefaultAsync();

        var nextNumber = 1;

        if (lastPatient != null)
        {
            // Extract the sequence number and increment it
            var lastNumberStr = lastPatient.PatientUniqueId.Substring(prefix.Length);
            if (int.TryParse(lastNumberStr, out var lastNumber))
            {
                nextNumber = lastNumber + 1;
            }
        }

        return $"{prefix}{nextNumber:D4}";
    }

    // Maps a Patient entity to a PatientResponseDto
    private static PatientResponseDto MapToResponse(Patient p) => new()
    {
        Id = p.Id,
        PatientUniqueId = p.PatientUniqueId,
        FirstName = p.FirstName,
        LastName = p.LastName,
        DateOfBirth = p.DateOfBirth,
        Gender = p.Gender,
        PhoneNumber = p.PhoneNumber,
        Address = p.Address,
        PatientType = p.PatientType,
        RegisteredAt = p.RegisteredAt
    };
}

What does this controller do?

  • [Authorize] on the class means every endpoint requires a valid JWT token. The [Authorize(Roles = "Admin,Receptionist")] on CreatePatient further restricts creation to those two roles.
  • CreatePatient reads the logged-in user's ID from the JWT claims, auto-generates a unique patient ID, saves the patient to the database, and returns a 201 Created response.
  • GeneratePatientUniqueId builds IDs in the format KRL-YYNNNN. It queries the database for the last patient registered in the current year and increments the sequence number. The first patient of the year gets 0001.
  • GetPatients returns all patients sorted by most recently registered. GetPatient returns a single patient by database ID, or 404 if not found.
  • MapToResponse converts a Patient entity into a PatientResponseDto, keeping the internal entity shape hidden from API consumers.

Why KRL-YYNNNN for patient IDs?

Hospital systems need human-readable patient identifiers that staff can reference in conversation and on printed forms. Auto-incrementing database IDs (1, 2, 3) work for internal lookups but don't convey any context.

The KRL prefix identifies Korutla Hospital. The two-digit year groups patients by registration year. The four-digit sequence supports up to 9,999 patients per year.

  • Save PatientsController.cs.
  • Verify the project compiles by running this in the terminal (make sure you are inside the HospitalApi folder):
dotnet build
  • You should see Build succeeded with 0 errors in the terminal output.

Build failed with errors?

  • Check that CreatePatientDto.cs and PatientResponseDto.cs are saved inside DTOs/ with the namespace HospitalApi.DTOs.
  • Make sure PatientsController.cs is inside the Controllers/ folder with the namespace HospitalApi.Controllers.
  • Confirm that all using statements at the top of each file match the code above exactly.

help me fix build errors in my PatientsController

✔️ Awesome, I've got everything!

Great. Double check you've saved all three new files before testing.

ⓧ I'd like to double check the full code

Here is the complete PatientsController.cs:

using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HospitalApi.Data;
using HospitalApi.DTOs;
using HospitalApi.Models;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class PatientsController : ControllerBase
{
    private readonly HospitalDbContext _context;

    public PatientsController(HospitalDbContext context)
    {
        _context = context;
    }

    [HttpPost]
    [Authorize(Roles = "Admin,Receptionist")]
    public async Task<IActionResult> CreatePatient([FromBody] CreatePatientDto dto)
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
        var uniqueId = await GeneratePatientUniqueId();

        var patient = new Patient
        {
            PatientUniqueId = uniqueId,
            FirstName = dto.FirstName,
            LastName = dto.LastName,
            DateOfBirth = dto.DateOfBirth,
            Gender = dto.Gender,
            PhoneNumber = dto.PhoneNumber,
            Address = dto.Address,
            PatientType = dto.PatientType,
            RegisteredAt = DateTime.UtcNow,
            RegisteredById = userId
        };

        _context.Patients.Add(patient);
        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetPatient), new { id = patient.Id }, MapToResponse(patient));
    }

    [HttpGet]
    public async Task<IActionResult> GetPatients()
    {
        var patients = await _context.Patients
            .OrderByDescending(p => p.RegisteredAt)
            .Select(p => MapToResponse(p))
            .ToListAsync();

        return Ok(patients);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetPatient(int id)
    {
        var patient = await _context.Patients.FindAsync(id);

        if (patient == null)
        {
            return NotFound();
        }

        return Ok(MapToResponse(patient));
    }

    private async Task<string> GeneratePatientUniqueId()
    {
        var year = DateTime.UtcNow.Year % 100;
        var prefix = $"KRL-{year:D2}";

        var lastPatient = await _context.Patients
            .Where(p => p.PatientUniqueId.StartsWith(prefix))
            .OrderByDescending(p => p.PatientUniqueId)
            .FirstOrDefaultAsync();

        var nextNumber = 1;

        if (lastPatient != null)
        {
            var lastNumberStr = lastPatient.PatientUniqueId.Substring(prefix.Length);
            if (int.TryParse(lastNumberStr, out var lastNumber))
            {
                nextNumber = lastNumber + 1;
            }
        }

        return $"{prefix}{nextNumber:D4}";
    }

    private static PatientResponseDto MapToResponse(Patient p) => new()
    {
        Id = p.Id,
        PatientUniqueId = p.PatientUniqueId,
        FirstName = p.FirstName,
        LastName = p.LastName,
        DateOfBirth = p.DateOfBirth,
        Gender = p.Gender,
        PhoneNumber = p.PhoneNumber,
        Address = p.Address,
        PatientType = p.PatientType,
        RegisteredAt = p.RegisteredAt
    };
}
Test the API endpoints

Your patient registration endpoints are built. Now you'll run the API and use curl to verify that creating and listing patients works end to end.

  • Start the API by running this command from the HospitalApi folder:
dotnet run

Which port is your API on?

When the API starts, the terminal prints a line like Now listening on: http://localhost:XXXX. Note this port number. The project code uses port 5057. If your port is different, replace 5057 in the curl commands below with your actual port.

  • Open a second terminal window. The API server is running in the first terminal so you need a separate one for curl commands.
  • Log in as the seeded admin user to get a JWT token by running:
curl -s -X POST http://localhost:5057/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@korutla.hospital", "password": "Admin@123"}'

What should I see?

You should see a JSON response containing a token, email, and role field. Copy the entire token value (the long string starting with eyJ). You'll paste it into the next commands.

  • Create a patient by running the command below. Replace your-jwt-token-here with the token you just copied:
curl -s -X POST http://localhost:5057/api/patients \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-jwt-token-here" \
  -d '{"firstName": "Ravi", "lastName": "Kumar", "dateOfBirth": "1990-05-15", "gender": "Male", "phoneNumber": "9876543210", "address": "Korutla, Telangana", "patientType": 0}'

What should I see?

You should see a JSON response with a patientUniqueId field showing something like KRL-260001. The two-digit year matches the current year. The patient data you sent is returned along with the auto-generated ID and a registeredAt timestamp.

  • Retrieve the patient list by running:
curl -s http://localhost:5057/api/patients \
  -H "Authorization: Bearer your-jwt-token-here"
  • You should see a JSON array containing the patient you just created, with the same patientUniqueId you saw in the POST response.

Getting a 401 Unauthorized response?

  • Make sure you copied the full token from the login response. It's a long string with no spaces or line breaks.
  • Check that the Authorization header format is exactly Bearer <token> with a single space between Bearer and the token.
  • JWT tokens expire after 8 hours. If you waited a long time between login and testing, log in again to get a fresh token.

help me debug 401 errors on my patient registration API

Getting a connection refused error?

  • Confirm the API is still running in your first terminal. You should see log output with no errors.
  • Double check the port number in your curl command matches the port printed in the API's startup output.

help me fix connection refused errors when testing my API with curl

Your patient registration API is working. Staff with the right roles can register patients, and every patient gets a unique, human-readable ID. Next up, you'll build the React frontend so hospital staff can use these endpoints through a web interface instead of curl.

Build the React Frontend with Login and Patient Registration

Your API is fully functional. You can authenticate users, assign roles, and register patients with auto-generated IDs. But right now, the only way to interact with it is through raw HTTP requests.

In this step, you will build the React frontend that hospital staff will actually use. You will create a login screen, a patient list, and a registration form that talks to your secured API.

In this step, get ready to:
  • Set up an Axios API client with JWT token injection and a React auth context.
  • Build login, patient list, and patient registration pages.
  • Wire up routing with React Router and test the full end-to-end flow.
Set up the API client and auth context

Every frontend page will need to call your API and include the logged-in user's JWT token. Instead of repeating that logic in every page, you will create two shared modules: an Axios client that automatically attaches the token to every request, and a React context that tracks whether the user is logged in.

  • In your terminal, make sure you are inside the hospital-frontend directory.
  • Create the src/api/ folder and the axiosClient.ts file by running these commands:
mkdir -p src/api
touch src/api/axiosClient.ts
  • Open src/api/axiosClient.ts in VS Code and paste the following code:
import axios from "axios";

// Create a shared Axios instance pointed at the API
const axiosClient = axios.create({
  baseURL: "http://localhost:5057",
});

// Attach the JWT token to every outgoing request
axiosClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default axiosClient;

What does this code do?

axios.create() builds a reusable HTTP client with a fixed base URL. Every call you make through axiosClient automatically targets http://localhost:5057 where your ASP.NET Core API runs.

The interceptor runs before every request and checks localStorage for a saved JWT token. If one exists, it attaches it as a Bearer header so your API knows who is making the request.

Check your API port

If your API runs on a different port than 5057, check Properties/launchSettings.json in your HospitalApi project for the HTTP port. Update the baseURL in axiosClient.ts to match.

  • Save axiosClient.ts.

You won't see a visible change yet. The next file will use this client.

  • Create the src/context/ folder and the AuthContext.tsx file by running these commands:
mkdir -p src/context
touch src/context/AuthContext.tsx
  • Open src/context/AuthContext.tsx and paste the following code:
import { createContext, useContext, useState, ReactNode } from "react";

// Shape of the logged-in user's data
interface AuthUser {
  token: string;
  email: string;
  role: string;
}

// What the context provides to consuming components
interface AuthContextType {
  user: AuthUser | null;
  login: (userData: AuthUser) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  // On mount, check localStorage for an existing session
  const [user, setUser] = useState<AuthUser | null>(() => {
    const token = localStorage.getItem("token");
    const email = localStorage.getItem("email");
    const role = localStorage.getItem("role");
    if (token && email && role) {
      return { token, email, role };
    }
    return null;
  });

  // Save user data to localStorage and state
  const login = (userData: AuthUser) => {
    localStorage.setItem("token", userData.token);
    localStorage.setItem("email", userData.email);
    localStorage.setItem("role", userData.role);
    setUser(userData);
  };

  // Clear user data from localStorage and state
  const logout = () => {
    localStorage.removeItem("token");
    localStorage.removeItem("email");
    localStorage.removeItem("role");
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook for any component to access auth state
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

What does this code do?

AuthProvider wraps the entire app and holds the current user's token, email, and role in React state. It also persists these values to localStorage so the session survives page refreshes.

The useAuth() hook gives any component access to the current user, a login() function to save credentials, and a logout() function to clear them.

  • Save AuthContext.tsx.
  • Confirm both new files exist by running:
ls src/api src/context

You should see axiosClient.ts listed under src/api and AuthContext.tsx listed under src/context.

Don't see the files?

Make sure you ran the mkdir -p commands from inside the hospital-frontend directory. Run pwd to confirm your current location ends with hospital-frontend.

Help me debug my project folder structure.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

import axios from "axios";

const axiosClient = axios.create({
  baseURL: "http://localhost:5057",
});

axiosClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default axiosClient;
import { createContext, useContext, useState, ReactNode } from "react";

interface AuthUser {
  token: string;
  email: string;
  role: string;
}

interface AuthContextType {
  user: AuthUser | null;
  login: (userData: AuthUser) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<AuthUser | null>(() => {
    const token = localStorage.getItem("token");
    const email = localStorage.getItem("email");
    const role = localStorage.getItem("role");
    if (token && email && role) {
      return { token, email, role };
    }
    return null;
  });

  const login = (userData: AuthUser) => {
    localStorage.setItem("token", userData.token);
    localStorage.setItem("email", userData.email);
    localStorage.setItem("role", userData.role);
    setUser(userData);
  };

  const logout = () => {
    localStorage.removeItem("token");
    localStorage.removeItem("email");
    localStorage.removeItem("role");
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}
Build the Login page and Patient Registration page

With the API client and auth context in place, you can now build the three pages your hospital staff will interact with: a login form, a patient list, and a patient registration form.

  • Create the src/pages/ folder by running:
mkdir -p src/pages
  • Create src/pages/LoginPage.tsx and paste the following code:
import { useState, FormEvent } from "react";
import { useNavigate } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

export default function LoginPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const navigate = useNavigate();
  const { login } = useAuth();

  // Submit credentials to the API login endpoint
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError("");
    try {
      const response = await axiosClient.post("/api/auth/login", {
        email,
        password,
      });
      login(response.data);
      navigate("/patients");
    } catch {
      setError("Invalid email or password.");
    }
  };

  return (
    <div style={{ maxWidth: 400, margin: "80px auto", padding: 20 }}>
      <h1>Korutla Hospital</h1>
      <h2>Staff Login</h2>
      {error && <p style={{ color: "red" }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: 12 }}>
          <label htmlFor="email">Email</label>
          <br />
          <input
            id="email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
            style={{ width: "100%", padding: 8 }}
          />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label htmlFor="password">Password</label>
          <br />
          <input
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
            style={{ width: "100%", padding: 8 }}
          />
        </div>
        <button type="submit" style={{ padding: "8px 24px" }}>
          Login
        </button>
      </form>
    </div>
  );
}

What does this code do?

LoginPage renders an email and password form. When the user submits, it calls POST /api/auth/login using your axiosClient.

If the login succeeds, the API returns a JWT token, email, and role. The login() function from your auth context stores these in localStorage and navigates the user to the patients page.

  • Save LoginPage.tsx.

You won't see the login form yet. It needs routing, which you will set up in the next substep.

  • Create src/pages/PatientsPage.tsx and paste the following code:
import { useEffect, useState } from "react";
import { Link } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

// Shape of a patient record from the API
interface Patient {
  id: number;
  patientUniqueId: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  gender: string;
  phoneNumber: string;
  patientType: string;
  registeredAt: string;
}

export default function PatientsPage() {
  const [patients, setPatients] = useState<Patient[]>([]);
  const { user, logout } = useAuth();

  // Fetch all patients when the page loads
  useEffect(() => {
    axiosClient.get("/api/patients").then((res) => setPatients(res.data));
  }, []);

  return (
    <div style={{ padding: 20 }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: 20,
        }}
      >
        <h1>Patients</h1>
        <div>
          <span>
            {user?.email} ({user?.role})
          </span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>
      <Link to="/patients/new">
        <button style={{ marginBottom: 16, padding: "8px 16px" }}>
          Register New Patient
        </button>
      </Link>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Patient ID</th>
            <th style={thStyle}>Name</th>
            <th style={thStyle}>Gender</th>
            <th style={thStyle}>Phone</th>
            <th style={thStyle}>Type</th>
            <th style={thStyle}>Registered</th>
          </tr>
        </thead>
        <tbody>
          {patients.map((p) => (
            <tr key={p.id}>
              <td style={tdStyle}>{p.patientUniqueId}</td>
              <td style={tdStyle}>
                {p.firstName} {p.lastName}
              </td>
              <td style={tdStyle}>{p.gender}</td>
              <td style={tdStyle}>{p.phoneNumber}</td>
              <td style={tdStyle}>{p.patientType}</td>
              <td style={tdStyle}>
                {new Date(p.registeredAt).toLocaleDateString()}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// Table header styling
const thStyle = {
  borderBottom: "2px solid #ccc",
  textAlign: "left" as const,
  padding: 8,
};

// Table cell styling
const tdStyle = {
  borderBottom: "1px solid #eee",
  padding: 8,
};

What does this code do?

PatientsPage fetches the full patient list from GET /api/patients when the component mounts. Because axiosClient automatically attaches the JWT token, the API accepts the request without any extra work.

The navbar shows the logged-in user's email and role, plus a logout button. The Register New Patient button links to /patients/new where the registration form will live.

  • Save PatientsPage.tsx.
  • Create src/pages/RegisterPatientPage.tsx and paste the following code:
import { useState, FormEvent } from "react";
import { useNavigate } from "react-router";
import axiosClient from "../api/axiosClient";

export default function RegisterPatientPage() {
  const navigate = useNavigate();
  // Form state with default values
  const [form, setForm] = useState({
    firstName: "",
    lastName: "",
    dateOfBirth: "",
    gender: "Male",
    phoneNumber: "",
    address: "",
    patientType: 0,
  });
  const [error, setError] = useState("");
  const [createdId, setCreatedId] = useState("");

  // Update form state when any input changes
  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
  ) => {
    const { name, value } = e.target;
    setForm((prev) => ({
      ...prev,
      [name]: name === "patientType" ? parseInt(value) : value,
    }));
  };

  // Submit the form to the patient registration endpoint
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError("");
    try {
      const response = await axiosClient.post("/api/patients", form);
      setCreatedId(response.data.patientUniqueId);
    } catch {
      setError("Failed to register patient. Check your inputs.");
    }
  };

  // Show success screen after registration
  if (createdId) {
    return (
      <div style={{ maxWidth: 500, margin: "80px auto", padding: 20 }}>
        <h2>Patient Registered Successfully</h2>
        <p>
          Patient ID: <strong>{createdId}</strong>
        </p>
        <button onClick={() => navigate("/patients")} style={{ padding: "8px 16px" }}>
          Back to Patients
        </button>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: 500, margin: "40px auto", padding: 20 }}>
      <h2>Register New Patient</h2>
      {error && <p style={{ color: "red" }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: 12 }}>
          <label>First Name</label>
          <br />
          <input name="firstName" value={form.firstName} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Last Name</label>
          <br />
          <input name="lastName" value={form.lastName} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Date of Birth</label>
          <br />
          <input name="dateOfBirth" type="date" value={form.dateOfBirth} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Gender</label>
          <br />
          <select name="gender" value={form.gender} onChange={handleChange} style={{ width: "100%", padding: 8 }}>
            <option value="Male">Male</option>
            <option value="Female">Female</option>
            <option value="Other">Other</option>
          </select>
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Phone Number</label>
          <br />
          <input name="phoneNumber" value={form.phoneNumber} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Address</label>
          <br />
          <input name="address" value={form.address} onChange={handleChange} style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Patient Type</label>
          <br />
          <select name="patientType" value={form.patientType} onChange={handleChange} style={{ width: "100%", padding: 8 }}>
            <option value={0}>Outpatient</option>
            <option value={1}>Inpatient</option>
          </select>
        </div>
        <button type="submit" style={{ padding: "8px 24px" }}>Register Patient</button>
      </form>
    </div>
  );
}

What does this code do?

RegisterPatientPage renders a form with fields for first name, last name, date of birth, gender, phone number, address, and patient type (Outpatient or Inpatient). These match the CreatePatientDto your API expects.

When the form submits, it POSTs to /api/patients. On success, the page switches to a confirmation screen showing the auto-generated PatientUniqueId (like KRL-260001) returned by the API.

  • Save RegisterPatientPage.tsx.
  • Confirm all three page files exist by running:
ls src/pages

You should see LoginPage.tsx, PatientsPage.tsx, and RegisterPatientPage.tsx in the output.

Missing a file?

Make sure you created each file inside the src/pages/ folder. Check that the filenames are exact, including the capital letters (LoginPage.tsx, not loginpage.tsx).

Help me find my missing page files in the hospital frontend.

✔️ Awesome, I've got everything!

Great. Double check all three page files are saved.

ⓧ I'd like to double check the full code

import { useState, FormEvent } from "react";
import { useNavigate } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

export default function LoginPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const navigate = useNavigate();
  const { login } = useAuth();

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError("");
    try {
      const response = await axiosClient.post("/api/auth/login", {
        email,
        password,
      });
      login(response.data);
      navigate("/patients");
    } catch {
      setError("Invalid email or password.");
    }
  };

  return (
    <div style={{ maxWidth: 400, margin: "80px auto", padding: 20 }}>
      <h1>Korutla Hospital</h1>
      <h2>Staff Login</h2>
      {error && <p style={{ color: "red" }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: 12 }}>
          <label htmlFor="email">Email</label>
          <br />
          <input
            id="email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
            style={{ width: "100%", padding: 8 }}
          />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label htmlFor="password">Password</label>
          <br />
          <input
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
            style={{ width: "100%", padding: 8 }}
          />
        </div>
        <button type="submit" style={{ padding: "8px 24px" }}>
          Login
        </button>
      </form>
    </div>
  );
}
import { useEffect, useState } from "react";
import { Link } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

interface Patient {
  id: number;
  patientUniqueId: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  gender: string;
  phoneNumber: string;
  patientType: string;
  registeredAt: string;
}

export default function PatientsPage() {
  const [patients, setPatients] = useState<Patient[]>([]);
  const { user, logout } = useAuth();

  useEffect(() => {
    axiosClient.get("/api/patients").then((res) => setPatients(res.data));
  }, []);

  return (
    <div style={{ padding: 20 }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: 20,
        }}
      >
        <h1>Patients</h1>
        <div>
          <span>
            {user?.email} ({user?.role})
          </span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>
      <Link to="/patients/new">
        <button style={{ marginBottom: 16, padding: "8px 16px" }}>
          Register New Patient
        </button>
      </Link>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Patient ID</th>
            <th style={thStyle}>Name</th>
            <th style={thStyle}>Gender</th>
            <th style={thStyle}>Phone</th>
            <th style={thStyle}>Type</th>
            <th style={thStyle}>Registered</th>
          </tr>
        </thead>
        <tbody>
          {patients.map((p) => (
            <tr key={p.id}>
              <td style={tdStyle}>{p.patientUniqueId}</td>
              <td style={tdStyle}>
                {p.firstName} {p.lastName}
              </td>
              <td style={tdStyle}>{p.gender}</td>
              <td style={tdStyle}>{p.phoneNumber}</td>
              <td style={tdStyle}>{p.patientType}</td>
              <td style={tdStyle}>
                {new Date(p.registeredAt).toLocaleDateString()}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

const thStyle = {
  borderBottom: "2px solid #ccc",
  textAlign: "left" as const,
  padding: 8,
};

const tdStyle = {
  borderBottom: "1px solid #eee",
  padding: 8,
};
import { useState, FormEvent } from "react";
import { useNavigate } from "react-router";
import axiosClient from "../api/axiosClient";

export default function RegisterPatientPage() {
  const navigate = useNavigate();
  const [form, setForm] = useState({
    firstName: "",
    lastName: "",
    dateOfBirth: "",
    gender: "Male",
    phoneNumber: "",
    address: "",
    patientType: 0,
  });
  const [error, setError] = useState("");
  const [createdId, setCreatedId] = useState("");

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
  ) => {
    const { name, value } = e.target;
    setForm((prev) => ({
      ...prev,
      [name]: name === "patientType" ? parseInt(value) : value,
    }));
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError("");
    try {
      const response = await axiosClient.post("/api/patients", form);
      setCreatedId(response.data.patientUniqueId);
    } catch {
      setError("Failed to register patient. Check your inputs.");
    }
  };

  if (createdId) {
    return (
      <div style={{ maxWidth: 500, margin: "80px auto", padding: 20 }}>
        <h2>Patient Registered Successfully</h2>
        <p>
          Patient ID: <strong>{createdId}</strong>
        </p>
        <button onClick={() => navigate("/patients")} style={{ padding: "8px 16px" }}>
          Back to Patients
        </button>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: 500, margin: "40px auto", padding: 20 }}>
      <h2>Register New Patient</h2>
      {error && <p style={{ color: "red" }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: 12 }}>
          <label>First Name</label>
          <br />
          <input name="firstName" value={form.firstName} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Last Name</label>
          <br />
          <input name="lastName" value={form.lastName} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Date of Birth</label>
          <br />
          <input name="dateOfBirth" type="date" value={form.dateOfBirth} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Gender</label>
          <br />
          <select name="gender" value={form.gender} onChange={handleChange} style={{ width: "100%", padding: 8 }}>
            <option value="Male">Male</option>
            <option value="Female">Female</option>
            <option value="Other">Other</option>
          </select>
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Phone Number</label>
          <br />
          <input name="phoneNumber" value={form.phoneNumber} onChange={handleChange} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Address</label>
          <br />
          <input name="address" value={form.address} onChange={handleChange} style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Patient Type</label>
          <br />
          <select name="patientType" value={form.patientType} onChange={handleChange} style={{ width: "100%", padding: 8 }}>
            <option value={0}>Outpatient</option>
            <option value={1}>Inpatient</option>
          </select>
        </div>
        <button type="submit" style={{ padding: "8px 24px" }}>Register Patient</button>
      </form>
    </div>
  );
}
Set up routing and run the full stack

Your pages are built, but the app does not know when to show each one. React Router maps URL paths like /login and /patients to the correct page components.

  • Install React Router by running:
npm install react-router

Install failed?

Make sure you are running this command from inside the hospital-frontend directory, not from HospitalApi or the root korutla-hospital folder. Run pwd to confirm.

Help me fix the npm install error for react-router.

  • Open src/App.tsx in VS Code and replace its entire contents with the following code:
import { BrowserRouter, Routes, Route, Navigate } from "react-router";
import { AuthProvider, useAuth } from "./context/AuthContext";
import LoginPage from "./pages/LoginPage";
import PatientsPage from "./pages/PatientsPage";
import RegisterPatientPage from "./pages/RegisterPatientPage";
import { ReactNode } from "react";

// Redirect to /login if the user is not authenticated
function ProtectedRoute({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" />;
  }
  return children;
}

// Define all routes inside AuthProvider so they can use useAuth
function AppRoutes() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/patients"
        element={
          <ProtectedRoute>
            <PatientsPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/patients/new"
        element={
          <ProtectedRoute>
            <RegisterPatientPage />
          </ProtectedRoute>
        }
      />
      <Route path="*" element={<Navigate to="/login" />} />
    </Routes>
  );
}

export default function App() {
  return (
    <BrowserRouter>
      <AuthProvider>
        <AppRoutes />
      </AuthProvider>
    </BrowserRouter>
  );
}

What does this code do?

ProtectedRoute checks if the user is logged in. If not, it redirects them to /login. This protects the patient pages from unauthenticated access.

The Routes block maps three URL paths: /login shows the login form, /patients shows the patient list, and /patients/new shows the registration form. Any other path redirects to login.

  • Save App.tsx.
  • Open src/main.tsx and find this line:
import './index.css'
  • Delete this line entirely. Your main.tsx should now look like this:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>
);
  • Save main.tsx.

Now run both the API and frontend together to test the full flow.

  • Open a terminal in the HospitalApi directory and start the API by running:
dotnet run
  • Open a second terminal in the hospital-frontend directory and start the frontend by running:
npm run dev
  • Open http://localhost:5173 in your browser. You should see the Korutla Hospital login form.
  • Enter admin@korutla.hospital in the Email field.
  • Enter Admin@123 in the Password field.
  • Click Login. You should be redirected to the Patients page, which shows your email and "Admin" role in the top-right corner.
  • Click Register New Patient.
  • Fill in the form with test data (any name, date of birth, phone number, and address).
  • Select Outpatient or Inpatient from the Patient Type dropdown.
  • Click Register Patient. You should see a success message displaying the auto-generated patient ID (for example, KRL-260001).
  • Click Back to Patients. Your new patient should appear in the table.

Login fails or page is blank?

If you see a blank page, open your browser's developer console (F12) and check for errors. A common issue is the baseURL in axiosClient.ts not matching the port your API is actually running on.

If login returns an error, make sure the API is running in its own terminal with dotnet run. Also verify you typed the credentials exactly: admin@korutla.hospital and Admin@123.

If you see a CORS error in the console, confirm that your API's Program.cs has the AllowFrontend CORS policy set to http://localhost:5173.

Help me debug my hospital frontend login or blank page issue.

✔️ Awesome, I've got everything!

Great. Make sure all your files are saved across both projects.

ⓧ I'd like to double check the full code

import { BrowserRouter, Routes, Route, Navigate } from "react-router";
import { AuthProvider, useAuth } from "./context/AuthContext";
import LoginPage from "./pages/LoginPage";
import PatientsPage from "./pages/PatientsPage";
import RegisterPatientPage from "./pages/RegisterPatientPage";
import { ReactNode } from "react";

function ProtectedRoute({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" />;
  }
  return children;
}

function AppRoutes() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/patients"
        element={
          <ProtectedRoute>
            <PatientsPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/patients/new"
        element={
          <ProtectedRoute>
            <RegisterPatientPage />
          </ProtectedRoute>
        }
      />
      <Route path="*" element={<Navigate to="/login" />} />
    </Routes>
  );
}

export default function App() {
  return (
    <BrowserRouter>
      <AuthProvider>
        <AppRoutes />
      </AuthProvider>
    </BrowserRouter>
  );
}
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Secret mission

Add Patient Search and Filtering

Your patients list is growing, and scrolling through every record to find a specific patient is not practical. In this secret mission, you will add query parameter support to the API and a search/filter bar in the React frontend so staff can quickly find patients by name, patient ID, or type (OP/IP).

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • PostgreSQL database korutla_hospital with Identity and Patients tables (local, no ongoing cost).
  • PostgreSQL user hospitaladmin.
  • Local project directory korutla-hospital containing HospitalApi and hospital-frontend.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the project soon.

  • Your PostgreSQL database persists locally with no ongoing cost.
  • Run dotnet run in the HospitalApi folder and npm run dev in the hospital-frontend folder whenever you want to continue.
  • The .NET 10 SDK, PostgreSQL, and the dotnet-ef tool will remain installed on your system.

Pause - I'll come back to this later

Shut down running processes to free up system memory. Your database and project files remain intact for when you return.

  • Stop both dev servers by pressing Ctrl+C in each terminal window.
  • Stop the PostgreSQL service to free up resources by running:
sudo systemctl stop postgresql
  • When you are ready to return, start PostgreSQL again by running:
sudo systemctl start postgresql

Delete - I don't want to use this again

Remove all project-specific resources and start fresh. This deletes the database, the database user, and both project folders.

  • Stop both dev servers by pressing Ctrl+C in each terminal window if they are still running.
  • Drop the korutla_hospital database by running:
sudo -u postgres psql -c "DROP DATABASE korutla_hospital;"
  • Drop the hospitaladmin database user by running:
sudo -u postgres psql -c "DROP USER hospitaladmin;"
  • Delete the project directory by running:
rm -rf ~/Desktop/korutla-hospital
  • Confirm the directory is gone by running:
ls ~/Desktop/korutla-hospital

You should see a message indicating the directory does not exist.

Nice Work!

Nice Work!

Nice work. You just built a full-stack hospital management system foundation for Korutla, complete with a secured ASP.NET Core Web API, a PostgreSQL database, and a React + TypeScript frontend where staff can log in and register patients with auto-generated unique IDs.

You've learned how to:

  • Build an ASP.NET Core 10 Web API backed by PostgreSQL, using Entity Framework Core code-first migrations to create a hospital database schema with Identity tables and a Patients table.
  • Implement JWT authentication with four hospital roles (Admin, Doctor, Receptionist, Billing) and role-based authorization to restrict API endpoints to specific staff members.
  • Create a React + TypeScript frontend with login, patient listing, and a patient registration form that auto-generates unique IDs in the KRL-YYNNNN format.
  • Secret Mission: Added patient search and filtering to find patients by name, unique ID, or patient type (OP/IP).

Ready to quiz yourself?