Skip to main content
MediGuide allows authenticated users to track comprehensive medical vitals that are validated, stored securely in PostgreSQL, and used to generate personalized health insights.

Overview

The medical tracking system captures:
  • Glucose levels (mg/dL)
  • Blood pressure - systolic and diastolic (mmHg)
  • Heart rate (beats per minute)
  • Oxygen saturation (SpO₂ %)
  • Body temperature (°C)
  • Respiratory rate (breaths per minute)
  • Weight (kg)
  • Height (m)
  • Age (years)
  • Blood type (A+, A-, B+, B-, AB+, AB-, O+, O-)
All medical data routes require JWT authentication. Users can only submit and view their own data.

Data Input Flow

1

Authenticate

User must be logged in with a valid JWT token.
2

Fill Medical Form

User enters their vital signs in the medical check form.
3

Client Validation

Frontend validates input ranges (e.g., glucose 0-999, oxygen 0-100%).
4

Submit to API

Data is sent to /api/medical-info with authentication header.
5

Server Validation

Backend validates using Zod schema to ensure data integrity.
6

Authorization Check

Server verifies user can only submit data for their own user ID.
7

Save to Database

Record is inserted into PostgreSQL with timestamp.
8

Confirmation

Success message returned with record ID.

Medical Data Form

Frontend Component

From src/components/medinfo.jsx:57-119:
function MedCheck({ userId, token }) {
  const [formData, setFormData] = useState({
    glucose: '', oxygenBlood: '', bloodPressureSystolic: '',
    bloodPressureDiastolic: '', temperature: '', age: '',
    height: '', weight: '', respiratoryRate: '', bloodType: '', heartRate: ''
  });

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor='Glucose'>Nivel de Glucosa (mg/dl)</label>
        <input
          type='number'
          name='glucose'
          value={formData.glucose}
          onChange={handleChange}
          min="0"
          max="999"
          required
        />
      </div>
      
      <div>
        <label htmlFor='OxigenBlood'>Oxigenación de la sangre (%)</label>
        <input
          type='number'
          name='oxygenBlood'
          value={formData.oxygenBlood}
          onChange={handleChange}
          min="0"
          max="100"
          required
        />
      </div>
      
      <div>
        <label htmlFor='BloodPresSystolic'>Presión Arterial Sistólica (mmHg)</label>
        <input
          type='number'
          name='bloodPressureSystolic'
          value={formData.bloodPressureSystolic}
          onChange={handleChange}
          min="0"
          max="300"
          required
        />
      </div>
      
      <div>
        <label htmlFor='BloodPresDiastolic'>Presión Arterial Diastólica (mmHg)</label>
        <input
          type='number'
          name='bloodPressureDiastolic'
          value={formData.bloodPressureDiastolic}
          onChange={handleChange}
          min="0"
          max="200"
          required
        />
      </div>
      
      <div>
        <label htmlFor='Temperature'>Temperatura Corporal (C°)</label>
        <input
          type='number'
          name='temperature'
          value={formData.temperature}
          onChange={handleChange}
          min="0"
          max="50"
          step="0.1"
          required
        />
      </div>
      
      <div>
        <label htmlFor='HeartRate'>Frecuencia Cardíaca (lat/min)</label>
        <input
          type='number'
          name='heartRate'
          value={formData.heartRate}
          onChange={handleChange}
          min="0"
          max="300"
          required
        />
      </div>
      
      <div>
        <label htmlFor='BloodType'>Tipo de Sangre</label>
        <select name='bloodType' value={formData.bloodType} onChange={handleChange} required>
          <option value=""></option>
          <option value="O+">O+</option>
          <option value="O-">O-</option>
          <option value="A+">A+</option>
          <option value="A-">A-</option>
          <option value="B+">B+</option>
          <option value="B-">B-</option>
          <option value="AB+">AB+</option>
          <option value="AB-">AB-</option>
        </select>
      </div>
      
      {/* Additional fields: weight, height, age, respiratory rate */}
      
      <button type='submit' disabled={loading}>
        {loading ? 'Guardando...' : 'Guardar Información'}
      </button>
    </form>
  );
}

Input Validation Ranges

  • Range: 0-999 mg/dL
  • Type: number
  • Required: Yes
  • Normal Range: 70-100 mg/dL (fasting)
  • Range: 0-100%
  • Type: number
  • Required: Yes
  • Normal Range: 95-100%
  • Systolic Range: 0-300 mmHg
  • Diastolic Range: 0-200 mmHg
  • Type: number
  • Required: Yes (both values)
  • Normal Range: Less than 120/80 mmHg
  • Range: 0-300 bpm
  • Type: number
  • Required: Yes
  • Normal Range: 60-100 bpm (adults at rest)
  • Range: 0-50°C
  • Type: number (0.1 step)
  • Required: Yes
  • Normal Range: 36.5-37.5°C
  • Range: 0-150 breaths/min
  • Type: number
  • Required: Yes
  • Normal Range: 12-20 breaths/min
  • Range: 0-500 kg
  • Type: number (0.1 step)
  • Required: Yes
  • Range: 0-3 meters
  • Type: number (0.01 step)
  • Required: Yes
  • Range: 0-150 years
  • Type: integer
  • Required: Yes
  • Options: A+, A-, B+, B-, AB+, AB-, O+, O-
  • Type: enum/select
  • Required: Yes

API Integration

Submitting Medical Data

POST /api/medical-info
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "userId": 42,
  "glucose": 95,
  "oxygenBlood": 98,
  "bloodPressureSystolic": 120,
  "bloodPressureDiastolic": 80,
  "temperature": 36.8,
  "age": 35,
  "height": 1.75,
  "weight": 70,
  "respiratoryRate": 16,
  "bloodType": "O+",
  "heartRate": 72
}

Frontend Submit Handler

From src/components/medinfo.jsx:22-55:
const handleSubmit = async (e) => {
  e.preventDefault();
  setLoading(true);
  setMessage('');

  try {
    const response = await fetch(`${API_URL}/api/medical-info`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify({ ...formData, userId })
    });

    const data = await response.json();

    if (response.ok) {
      setMessage('¡Información guardada exitosamente!');
      medicalDataEmitter.emit(); // Notify other components
      // Reset form
      setFormData({
        glucose: '', oxygenBlood: '', bloodPressureSystolic: '',
        bloodPressureDiastolic: '', temperature: '', age: '',
        height: '', weight: '', respiratoryRate: '', bloodType: '', heartRate: ''
      });
    } else {
      setMessage(`Error: ${data.error}`);
    }
  } catch (error) {
    setMessage(`Error de conexión: ${error.message}`);
  } finally {
    setLoading(false);
  }
};

Backend Implementation

Route Handler

From src/routes/medical.js:1-45:
import { Router } from 'express';
import pool from '../../db.js';
import auth from '../middleware/auth.js';
import { medicalSchema } from '../validators/medical.js';

const router = Router();

// All medical routes require authentication
router.use(auth);

router.post('/', async (req, res) => {
  try {
    // Validate with Zod schema
    const parsed = medicalSchema.safeParse(req.body);

    if (!parsed.success) {
      return res.status(400).json({
        error: 'Datos médicos inválidos',
        detalles: parsed.error.flatten().fieldErrors
      });
    }

    const {
      userId, glucose, oxygenBlood, bloodPressureSystolic, bloodPressureDiastolic,
      temperature, age, height, weight, respiratoryRate, bloodType, heartRate,
    } = parsed.data;

    // Authorization: users can only save their own data
    if (req.user.id !== userId) {
      return res.status(403).json({ error: 'No autorizado para guardar datos de otro usuario' });
    }

    const { rows } = await pool.query(
      `INSERT INTO medical_records
         (user_id, glucose, oxygen_blood, blood_pressure_systolic, blood_pressure_diastolic,
          temperature, age, height, weight, respiratory_rate, blood_type, heart_rate, created_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW())
       RETURNING id`,
      [userId, glucose, oxygenBlood, bloodPressureSystolic, bloodPressureDiastolic,
       temperature, age, height, weight, respiratoryRate, bloodType, heartRate]
    );

    console.log(`[medical] Registro guardado: id=${rows[0].id}, userId=${userId}`);
    return res.status(201).json({ message: 'Registro médico guardado', id: rows[0].id });
  } catch (error) {
    console.error('[medical] Error al guardar registro:', error.message);
    return res.status(500).json({ error: 'Error interno del servidor' });
  }
});

export default router;
The route includes an authorization check to ensure users can only submit data for their own user ID, preventing unauthorized data injection.

Data Validation

Zod Schema

From src/validators/medical.js:1-18:
import { z } from 'zod';

const toNumber = z.union([z.number(), z.string()]).transform(val => Number(val));

export const medicalSchema = z.object({
  userId:                   toNumber,
  glucose:                  toNumber,
  oxygenBlood:              toNumber,
  bloodPressureSystolic:    toNumber,
  bloodPressureDiastolic:   toNumber,
  temperature:              toNumber,
  age:                      toNumber,
  height:                   toNumber,
  weight:                   toNumber,
  respiratoryRate:          toNumber,
  bloodType:                z.enum(['A+','A-','B+','B-','AB+','AB-','O+','O-']),
  heartRate:                toNumber,
});

Key Validation Features

Flexible Number Parsing

The schema accepts both strings and numbers, automatically converting strings to numbers. This handles form data where inputs are strings:
const toNumber = z.union([z.number(), z.string()]).transform(val => Number(val));

Blood Type Enum

Blood type is strictly validated against the 8 valid blood types:
bloodType: z.enum(['A+','A-','B+','B-','AB+','AB-','O+','O-'])

Error Details

Validation errors return detailed field-level feedback:
if (!parsed.success) {
  return res.status(400).json({
    error: 'Datos médicos inválidos',
    detalles: parsed.error.flatten().fieldErrors
  });
}

Database Storage

Medical Records Table

From initDb.js:17-34:
CREATE TABLE IF NOT EXISTS medical_records (
  id SERIAL PRIMARY KEY,
  user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
  glucose NUMERIC,
  oxygen_blood NUMERIC,
  blood_pressure_systolic NUMERIC,
  blood_pressure_diastolic NUMERIC,
  temperature NUMERIC,
  age INTEGER,
  height NUMERIC,
  weight NUMERIC,
  respiratory_rate NUMERIC,
  blood_type VARCHAR(2),
  heart_rate NUMERIC,
  created_at TIMESTAMP DEFAULT NOW()
);

Key Database Features

user_id references the users table with CASCADE delete:
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
This ensures that when a user is deleted, all their medical records are automatically removed.
Each record automatically stores creation time:
created_at TIMESTAMP DEFAULT NOW()
This enables time-series analysis and tracking health trends over time.
Vitals use NUMERIC type for precise decimal storage:
glucose NUMERIC,
temperature NUMERIC,
height NUMERIC

Retrieving Medical Data

Get Latest Record

From src/routes/medical.js:49-79:
router.get('/latest', async (req, res) => {
  try {
    const { userId } = req.query;

    if (!userId) {
      return res.status(400).json({ error: 'userId es requerido' });
    }

    // Authorization: users can only query their own records
    if (req.user.id !== Number(userId)) {
      return res.status(403).json({ error: 'No autorizado para consultar datos de otro usuario' });
    }

    const { rows } = await pool.query(
      `SELECT * FROM medical_records
       WHERE user_id = $1
       ORDER BY created_at DESC
       LIMIT 1`,
      [userId]
    );

    if (rows.length === 0) {
      return res.status(404).json({ error: 'No se encontraron registros médicos' });
    }

    return res.json(rows[0]);
  } catch (error) {
    console.error('[medical] Error al obtener registro:', error.message);
    return res.status(500).json({ error: 'Error interno del servidor' });
  }
});

Frontend Data Fetching

From src/components/healthplan.jsx:22-45:
const fetchMedicalData = async () => {
  try {
    const response = await fetch(`${API_URL}/api/medical-info/latest?userId=${userId}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    const data = await response.json();

    if (response.ok) {
      setMedicalData(data);
      analyzeHealth(data);
      setError('');
    } else {
      setMedicalData(null);
      setError('No hay datos médicos registrados. Por favor, completa el formulario.');
    }
  } catch (err) {
    setMedicalData(null);
    setError('Error al obtener datos médicos: ' + err.message);
  } finally {
    setLoading(false);
  }
};

Real-time Updates

Event Emitter Pattern

From src/utils/medicalDataContext.js:
class EventEmitter {
  constructor() {
    this.listeners = [];
  }

  emit() {
    this.listeners.forEach(callback => callback());
  }

  subscribe(callback) {
    this.listeners.push(callback);
    return () => {
      this.listeners = this.listeners.filter(cb => cb !== callback);
    };
  }
}

export const medicalDataEmitter = new EventEmitter();

Usage

When data is saved, emit an event:
if (response.ok) {
  setMessage('¡Información guardada exitosamente!');
  medicalDataEmitter.emit(); // Notify dashboard to refresh
}
Components subscribe to updates:
useEffect(() => {
  fetchMedicalData();
  const unsubscribe = medicalDataEmitter.subscribe(() => {
    fetchMedicalData(); // Refresh when new data saved
  });
  return unsubscribe;
}, []);
This pattern ensures the health dashboard automatically updates when new medical data is submitted, without requiring manual page refresh.

Security Considerations

  1. Authentication Required: All medical routes use the auth middleware
    router.use(auth);
    
  2. Authorization Checks: Users can only submit/view their own data
    if (req.user.id !== userId) {
      return res.status(403).json({ error: 'No autorizado' });
    }
    
  3. Input Validation: Zod schema prevents invalid data
    const parsed = medicalSchema.safeParse(req.body);
    
  4. SQL Injection Prevention: Parameterized queries
    pool.query(`SELECT * FROM medical_records WHERE user_id = $1`, [userId])
    

Best Practices

Regular Tracking

Encourage users to input data regularly for trend analysis:
  • Daily tracking of critical vitals (glucose for diabetics)
  • Weekly general health checks
  • Immediate logging after doctor visits

Data Accuracy

Ensure accurate measurements:
  • Use calibrated medical devices
  • Measure at consistent times
  • Rest before taking vitals
  • Follow device instructions

Privacy Protection

Medical data is highly sensitive:
  • Never share authentication tokens
  • Log out on shared devices
  • Review access logs regularly
  • Report suspicious activity

Build docs developers (and LLMs) love