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
-
Install Plugins
- Lombok
- Spring Boot
- Database Navigator
-
Configure Project
# Open project
File → Open → Select ciyex directory
# Import Gradle project
# IntelliJ will auto-detect build.gradle -
Code Style
# Import code style
File → Settings → Editor → Code Style
# Import from: .idea/codeStyles/Project.xml
VS Code
-
Install Extensions
- Extension Pack for Java
- Spring Boot Extension Pack
- Lombok Annotations Support
- ESLint
- Prettier
-
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 featurefix- Bug fixdocs- Documentationrefactor- Code refactoringtest- Adding testschore- Maintenance tasksperf- Performance improvements
Pull Request Process
-
Create Branch
git checkout -b feature/patient-search -
Make Changes
# Make your changes
git add .
git commit -m "feat(patients): add patient search functionality" -
Push Branch
git push origin feature/patient-search -
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]
-
Code Review
- Address reviewer comments
- Make requested changes
- Push updates
-
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:
- Set breakpoints in code
- Click "Debug" button
- 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
- Write Tests - Aim for 80%+ code coverage
- Code Reviews - All code must be reviewed
- Keep PRs Small - Max 400 lines changed
- Document Code - Add JSDoc/JavaDoc for public APIs
- Follow Standards - Use linters and formatters
- Security First - Never commit secrets
- Performance - Profile before optimizing
Next Steps
- Testing Guide - Detailed testing practices
- API Documentation - API reference
- Contributing - Contribution guidelines
- Code Review - Review checklist