Skip to main content
Version: v0.1.0

Development Guide

Complete guide for developers contributing to Ciyex EHR.

Overview

This guide covers the development workflow, coding standards, testing practices, and contribution guidelines for Ciyex EHR.

Development Environment

Required Tools

  • Java 21 - Backend development
  • Node.js 22 - Frontend development
  • pnpm 8+ - Package management
  • PostgreSQL 16 - Database
  • Git - Version control
  • IDE - IntelliJ IDEA or VS Code

IDE Setup

IntelliJ IDEA

  1. Install Plugins

    • Lombok
    • Spring Boot
    • Database Navigator
  2. Configure Project

    # Open project
    File → Open → Select ciyex directory

    # Import Gradle project
    # IntelliJ will auto-detect build.gradle
  3. Code Style

    # Import code style
    File → Settings → Editor → Code Style
    # Import from: .idea/codeStyles/Project.xml

VS Code

  1. Install Extensions

    • Extension Pack for Java
    • Spring Boot Extension Pack
    • Lombok Annotations Support
    • ESLint
    • Prettier
  2. Configure Settings

    {
    "java.configuration.updateBuildConfiguration": "automatic",
    "java.compile.nullAnalysis.mode": "automatic",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
    "source.organizeImports": true
    }
    }

Project Structure

Backend (Spring Boot)

ciyex/
├── src/
│ ├── main/
│ │ ├── java/com/qiaben/ciyex/
│ │ │ ├── config/ # Configuration classes
│ │ │ ├── controller/ # REST controllers
│ │ │ ├── service/ # Business logic
│ │ │ ├── repository/ # Data access
│ │ │ ├── model/ # Entity models
│ │ │ ├── dto/ # Data transfer objects
│ │ │ ├── security/ # Security configuration
│ │ │ ├── exception/ # Custom exceptions
│ │ │ └── util/ # Utility classes
│ │ └── resources/
│ │ ├── application.yml # Configuration
│ │ ├── db/migration/ # Flyway migrations
│ │ └── templates/ # Email templates
│ └── test/
│ └── java/com/qiaben/ciyex/
│ ├── controller/ # Controller tests
│ ├── service/ # Service tests
│ └── integration/ # Integration tests
├── build.gradle # Build configuration
└── gradle.properties # Gradle properties

Frontend (Next.js)

ciyex-ehr-ui/
├── src/
│ ├── app/ # Next.js app directory
│ │ ├── (auth)/ # Auth routes
│ │ ├── (dashboard)/ # Dashboard routes
│ │ ├── api/ # API routes
│ │ └── layout.tsx # Root layout
│ ├── components/ # React components
│ │ ├── ui/ # UI components
│ │ ├── forms/ # Form components
│ │ └── charts/ # Chart components
│ ├── lib/ # Utility functions
│ ├── hooks/ # Custom React hooks
│ ├── types/ # TypeScript types
│ └── styles/ # CSS styles
├── public/ # Static assets
├── package.json # Dependencies
└── tsconfig.json # TypeScript config

Coding Standards

Java

Naming Conventions:

// Classes: PascalCase
public class PatientService {}

// Methods: camelCase
public Patient findPatientById(Long id) {}

// Constants: UPPER_SNAKE_CASE
private static final String DEFAULT_SCHEMA = "public";

// Variables: camelCase
private String patientName;

Code Style:

// ✅ GOOD
@Service
@RequiredArgsConstructor
public class PatientService {
private final PatientRepository patientRepository;

public Patient createPatient(PatientDTO dto) {
// Validate input
validatePatientData(dto);

// Create entity
Patient patient = new Patient();
patient.setFirstName(dto.getFirstName());
patient.setLastName(dto.getLastName());

// Save and return
return patientRepository.save(patient);
}

private void validatePatientData(PatientDTO dto) {
if (dto.getFirstName() == null || dto.getFirstName().isBlank()) {
throw new ValidationException("First name is required");
}
}
}

// ❌ BAD
public class PatientService {
@Autowired
private PatientRepository patientRepository; // Use constructor injection

public Patient createPatient(PatientDTO dto) {
Patient patient = new Patient();
patient.setFirstName(dto.getFirstName()); // No validation
return patientRepository.save(patient);
}
}

Documentation:

/**
* Service for managing patient records.
*
* <p>This service provides CRUD operations for patients and handles
* business logic related to patient management.</p>
*
* @author Jane Smith
* @since 1.0.0
*/
@Service
public class PatientService {

/**
* Creates a new patient record.
*
* @param dto the patient data transfer object
* @return the created patient entity
* @throws ValidationException if patient data is invalid
*/
public Patient createPatient(PatientDTO dto) {
// Implementation
}
}

TypeScript/React

Naming Conventions:

// Components: PascalCase
export function PatientList() {}

// Functions: camelCase
function fetchPatients() {}

// Constants: UPPER_SNAKE_CASE
const API_BASE_URL = 'http://localhost:8080';

// Types/Interfaces: PascalCase
interface Patient {
id: number;
firstName: string;
}

Component Style:

// ✅ GOOD
'use client';

import { useState, useEffect } from 'react';
import { Patient } from '@/types';

interface PatientListProps {
organizationId: number;
}

export function PatientList({ organizationId }: PatientListProps) {
const [patients, setPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetchPatients();
}, [organizationId]);

const fetchPatients = async () => {
try {
const response = await fetch(`/api/patients?org=${organizationId}`);
const data = await response.json();
setPatients(data);
} catch (error) {
console.error('Failed to fetch patients:', error);
} finally {
setLoading(false);
}
};

if (loading) return <div>Loading...</div>;

return (
<div className="patient-list">
{patients.map(patient => (
<div key={patient.id}>{patient.firstName} {patient.lastName}</div>
))}
</div>
);
}

// ❌ BAD
export function PatientList(props) { // No types
const [patients, setPatients] = useState([]);

// No error handling
fetch('/api/patients').then(r => r.json()).then(setPatients);

return <div>{patients.map(p => <div>{p.firstName}</div>)}</div>; // No keys
}

Git Workflow

Branch Naming

# Feature branches
feature/patient-search
feature/appointment-reminders

# Bug fixes
bugfix/login-error
bugfix/date-formatting

# Hotfixes
hotfix/security-patch
hotfix/critical-bug

Commit Messages

Follow Conventional Commits:

# Format
<type>(<scope>): <description>

# Examples
feat(patients): add patient search functionality
fix(auth): resolve token expiration issue
docs(readme): update installation instructions
refactor(billing): simplify invoice calculation
test(appointments): add unit tests for scheduling
chore(deps): update Spring Boot to 4.0.1

Types:

  • feat - New feature
  • fix - Bug fix
  • docs - Documentation
  • refactor - Code refactoring
  • test - Adding tests
  • chore - Maintenance tasks
  • perf - Performance improvements

Pull Request Process

  1. Create Branch

    git checkout -b feature/patient-search
  2. Make Changes

    # Make your changes
    git add .
    git commit -m "feat(patients): add patient search functionality"
  3. Push Branch

    git push origin feature/patient-search
  4. Create Pull Request

    • Go to GitHub
    • Click "New Pull Request"
    • Fill in template:
      ## Description
      Adds patient search functionality with filters for name, DOB, and MRN.

      ## Changes
      - Added PatientSearchController
      - Implemented search service
      - Created search UI component
      - Added unit tests

      ## Testing
      - [ ] Unit tests pass
      - [ ] Integration tests pass
      - [ ] Manual testing completed

      ## Screenshots
      [Add screenshots if UI changes]
  5. Code Review

    • Address reviewer comments
    • Make requested changes
    • Push updates
  6. Merge

    • Squash and merge
    • Delete branch

Testing

Unit Tests (Backend)

@SpringBootTest
class PatientServiceTest {

@Mock
private PatientRepository patientRepository;

@InjectMocks
private PatientService patientService;

@Test
void createPatient_ValidData_ReturnsPatient() {
// Arrange
PatientDTO dto = new PatientDTO();
dto.setFirstName("John");
dto.setLastName("Doe");

Patient patient = new Patient();
patient.setId(1L);
patient.setFirstName("John");
patient.setLastName("Doe");

when(patientRepository.save(any(Patient.class)))
.thenReturn(patient);

// Act
Patient result = patientService.createPatient(dto);

// Assert
assertNotNull(result);
assertEquals("John", result.getFirstName());
assertEquals("Doe", result.getLastName());
verify(patientRepository).save(any(Patient.class));
}

@Test
void createPatient_InvalidData_ThrowsException() {
// Arrange
PatientDTO dto = new PatientDTO();
dto.setFirstName(""); // Invalid

// Act & Assert
assertThrows(ValidationException.class, () -> {
patientService.createPatient(dto);
});
}
}

Integration Tests (Backend)

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class PatientControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@Test
@WithMockUser(roles = "PROVIDER")
void createPatient_ValidRequest_Returns201() throws Exception {
PatientDTO dto = new PatientDTO();
dto.setFirstName("John");
dto.setLastName("Doe");

mockMvc.perform(post("/api/patients")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.firstName").value("John"));
}
}

Unit Tests (Frontend)

import { render, screen, fireEvent } from '@testing-library/react';
import { PatientList } from './PatientList';

describe('PatientList', () => {
it('renders patient list', async () => {
const mockPatients = [
{ id: 1, firstName: 'John', lastName: 'Doe' },
{ id: 2, firstName: 'Jane', lastName: 'Smith' }
];

global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockPatients)
})
) as jest.Mock;

render(<PatientList organizationId={1} />);

expect(await screen.findByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});

it('handles fetch error', async () => {
global.fetch = jest.fn(() => Promise.reject('API error'));

render(<PatientList organizationId={1} />);

expect(await screen.findByText('Error loading patients')).toBeInTheDocument();
});
});

Running Tests

# Backend tests
./gradlew test

# Frontend tests
pnpm test

# Coverage report
./gradlew jacocoTestReport
pnpm test --coverage

Database Migrations

Creating Migrations

# Create new migration file
# Format: V{version}__{description}.sql
# Example: V1.0.1__add_patient_email.sql
-- V1.0.1__add_patient_email.sql
ALTER TABLE patients
ADD COLUMN email VARCHAR(255);

CREATE INDEX idx_patients_email ON patients(email);

Running Migrations

# Migrations run automatically on startup
# Or manually:
./gradlew flywayMigrate

Debugging

Backend Debugging

IntelliJ IDEA:

  1. Set breakpoints in code
  2. Click "Debug" button
  3. Application starts in debug mode

Remote Debugging:

# Start with debug flags
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar

# In IntelliJ: Run → Edit Configurations → Add Remote JVM Debug
# Host: localhost, Port: 5005

Frontend Debugging

Browser DevTools:

// Add debugger statement
function fetchPatients() {
debugger; // Execution will pause here
fetch('/api/patients')...
}

VS Code Debugging:

// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug",
"type": "node",
"request": "launch",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["dev"],
"port": 9229
}
]
}

Performance Optimization

Backend

Database Query Optimization:

// ❌ BAD - N+1 query problem
List<Patient> patients = patientRepository.findAll();
for (Patient patient : patients) {
List<Appointment> appointments = patient.getAppointments(); // N queries
}

// ✅ GOOD - Fetch join
@Query("SELECT p FROM Patient p LEFT JOIN FETCH p.appointments")
List<Patient> findAllWithAppointments();

Caching:

@Cacheable(value = "patients", key = "#id")
public Patient findById(Long id) {
return patientRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Patient not found"));
}

@CacheEvict(value = "patients", key = "#patient.id")
public Patient updatePatient(Patient patient) {
return patientRepository.save(patient);
}

Frontend

Code Splitting:

// Lazy load components
const PatientChart = lazy(() => import('./PatientChart'));

function App() {
return (
<Suspense fallback={<Loading />}>
<PatientChart />
</Suspense>
);
}

Memoization:

// Memoize expensive calculations
const sortedPatients = useMemo(() => {
return patients.sort((a, b) => a.lastName.localeCompare(b.lastName));
}, [patients]);

// Memoize callbacks
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

Best Practices

  1. Write Tests - Aim for 80%+ code coverage
  2. Code Reviews - All code must be reviewed
  3. Keep PRs Small - Max 400 lines changed
  4. Document Code - Add JSDoc/JavaDoc for public APIs
  5. Follow Standards - Use linters and formatters
  6. Security First - Never commit secrets
  7. Performance - Profile before optimizing

Next Steps