16-Tutorial Paso a Paso - Consumir API Laravel con Token en React


馃殌 Paso 1: Configuraci贸n Inicial (5 minutos)

bash
# 1.1 Crear proyecto React
npx create-react-app gestor-tareas-token
cd gestor-tareas-token

# 1.2 Eliminar archivos innecesarios
rm src/App.test.js src/logo.svg src/reportWebVitals.js src/setupTests.js

# 1.3 Iniciar el proyecto
npm start

馃搧 Paso 2: Estructura de Carpetas (2 minutos)

text
src/
├── components/
│   ├── TareaList.js
│   ├── TareaForm.js
│   └── TareaItem.js
├── services/
│   └── tareaService.js
├── utils/
│   └── tokenManager.js
├── App.js
└── index.js

馃搫 Paso 3: index.js (1 minuto)

jsx
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

馃摝 Paso 4: App.js inicial (2 minutos)

jsx
// src/App.js
import React from 'react';
import TareaList from './components/TareaList';

function App() {
  return (
    <div>
      <h1>馃摑 Gestor de Tareas con Token</h1>
      <p>API Laravel con autenticaci贸n por token</p>
      <TareaList />
    </div>
  );
}

export default App;

馃攽 Paso 5: Gestor de Token (10 minutos)

jsx
// src/utils/tokenManager.js

class TokenManager {
  constructor() {
    this.tokenKey = 'api_token';
  }

  // Obtener token del localStorage
  getToken() {
    return localStorage.getItem(this.tokenKey);
  }

  // Guardar token en localStorage
  saveToken(token) {
    localStorage.setItem(this.tokenKey, token);
  }

  // Eliminar token
  clearToken() {
    localStorage.removeItem(this.tokenKey);
  }

  // Verificar si hay token
  hasToken() {
    return !!this.getToken();
  }

  // Generar nuevo token desde la API
  async generarNuevoToken() {
    try {
      const response = await fetch('http://localhost:8000/api/token/generar', {
        method: 'POST'
      });
      
      if (!response.ok) {
        throw new Error('Error al generar token');
      }
      
      const data = await response.json();
      
      // Guardar el token
      this.saveToken(data.token);
      
      return {
        success: true,
        token: data.token,
        message: 'Token generado y guardado'
      };
      
    } catch (error) {
      console.error('Error:', error);
      return {
        success: false,
        message: error.message
      };
    }
  }

  // Forzar generaci贸n de token si no existe
  async asegurarToken() {
    if (!this.hasToken()) {
      console.log('No hay token, generando uno nuevo...');
      return await this.generarNuevoToken();
    }
    return { success: true, token: this.getToken() };
  }
}

// Crear una instancia 煤nica
const tokenManager = new TokenManager();
export default tokenManager;

馃攲 Paso 6: Servicio de API con Token (15 minutos)

jsx
// src/services/tareaService.js
import tokenManager from '../utils/tokenManager';

const API_URL = 'http://localhost:8000/api';

const tareaService = {
  // Configuraci贸n com煤n para todas las peticiones
  async configurarPeticion(metodo, datos = null) {
    // Asegurar que tenemos un token
    const tokenResult = await tokenManager.asegurarToken();
    
    if (!tokenResult.success) {
      throw new Error('No se pudo obtener token: ' + tokenResult.message);
    }

    const token = tokenResult.token;
    
    // Configurar headers
    const config = {
      method: metodo,
      headers: {
        'Content-Type': 'application/json',
        'Token': token // Enviar token en el header
      }
    };

    // Agregar body si hay datos
    if (datos) {
      config.body = JSON.stringify(datos);
    }

    return config;
  },

  // 1. Obtener todas las tareas
  async obtenerTodas() {
    try {
      const config = await this.configurarPeticion('GET');
      
      const respuesta = await fetch(`${API_URL}/tareas`, config);
      
      if (respuesta.status === 401) {
        // Token inv谩lido, generar uno nuevo y reintentar
        await tokenManager.generarNuevoToken();
        return await this.obtenerTodas(); // Reintentar
      }
      
      if (!respuesta.ok) {
        throw new Error('Error al obtener tareas');
      }
      
      const data = await respuesta.json();
      return data.tareas || data;
      
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  },

  // 2. Crear nueva tarea
  async crearTarea(tarea) {
    try {
      const config = await this.configurarPeticion('POST', tarea);
      
      const respuesta = await fetch(`${API_URL}/tareas`, config);
      
      if (respuesta.status === 401) {
        await tokenManager.generarNuevoToken();
        return await this.crearTarea(tarea);
      }
      
      if (!respuesta.ok) {
        throw new Error('Error al crear tarea');
      }
      
      return await respuesta.json();
      
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  },

  // 3. Actualizar tarea (solo para completar)
  async actualizarTarea(id, datos) {
    try {
      const config = await this.configurarPeticion('PUT', datos);
      
      const respuesta = await fetch(`${API_URL}/tareas/${id}`, config);
      
      if (respuesta.status === 401) {
        await tokenManager.generarNuevoToken();
        return await this.actualizarTarea(id, datos);
      }
      
      if (!respuesta.ok) {
        throw new Error('Error al actualizar tarea');
      }
      
      return await respuesta.json();
      
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  },

  // 4. Eliminar tarea
  async eliminarTarea(id) {
    try {
      const config = await this.configurarPeticion('DELETE');
      
      const respuesta = await fetch(`${API_URL}/tareas/${id}`, config);
      
      if (respuesta.status === 401) {
        await tokenManager.generarNuevoToken();
        return await this.eliminarTarea(id);
      }
      
      if (!respuesta.ok) {
        throw new Error('Error al eliminar tarea');
      }
      
      return await respuesta.json();
      
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  },

  // 5. Obtener token actual
  getTokenActual() {
    return tokenManager.getToken();
  },

  // 6. Generar nuevo token manualmente
  async generarNuevoTokenManual() {
    return await tokenManager.generarNuevoToken();
  },

  // 7. Limpiar token
  limpiarToken() {
    tokenManager.clearToken();
  }
};

export default tareaService;

馃搵 Paso 7: TareaList.js con Token (15 minutos)

jsx
// src/components/TareaList.js
import React, { useState, useEffect } from 'react';
import TareaForm from './TareaForm';
import TareaItem from './TareaItem';
import tareaService from '../services/tareaService';

function TareaList() {
  const [tareas, setTareas] = useState([]);
  const [cargando, setCargando] = useState(true);
  const [error, setError] = useState('');
  const [tokenInfo, setTokenInfo] = useState('');

  // CSS Grid para estructura
  const estiloContenedor = {
    display: 'grid',
    gridTemplateColumns: '300px 1fr',
    gap: '20px',
    padding: '20px'
  };

  const estiloColumna = {
    border: '2px solid #333',
    padding: '15px'
  };

  const estiloTokenPanel = {
    background: '#e3f2fd',
    padding: '10px',
    marginBottom: '15px',
    borderRadius: '5px',
    border: '1px solid #bbdefb'
  };

  // Cargar tareas al iniciar
  useEffect(() => {
    cargarTareas();
    actualizarTokenInfo();
  }, []);

  // Mostrar informaci贸n del token
  const actualizarTokenInfo = () => {
    const token = tareaService.getTokenActual();
    setTokenInfo(token ? `Token: ${token}` : 'No hay token');
  };

  const cargarTareas = async () => {
    try {
      setCargando(true);
      setError('');
      const datos = await tareaService.obtenerTodas();
      setTareas(datos);
      actualizarTokenInfo();
    } catch (error) {
      setError(`❌ Error: ${error.message}`);
      console.error('Error detallado:', error);
    } finally {
      setCargando(false);
    }
  };

  const manejarCrearTarea = async (nuevaTarea) => {
    try {
      await tareaService.crearTarea(nuevaTarea);
      cargarTareas();
    } catch (error) {
      alert('Error al crear la tarea: ' + error.message);
    }
  };

  const manejarEliminarTarea = async (id) => {
    if (window.confirm('¿Eliminar esta tarea?')) {
      try {
        await tareaService.eliminarTarea(id);
        cargarTareas();
      } catch (error) {
        alert('Error al eliminar la tarea: ' + error.message);
      }
    }
  };

  const manejarCompletarTarea = async (id, completada) => {
    try {
      await tareaService.actualizarTarea(id, { completada });
      cargarTareas();
    } catch (error) {
      alert('Error al actualizar la tarea: ' + error.message);
    }
  };

  const generarNuevoToken = async () => {
    try {
      const resultado = await tareaService.generarNuevoTokenManual();
      if (resultado.success) {
        alert(`✅ Nuevo token generado: ${resultado.token}`);
        actualizarTokenInfo();
        cargarTareas();
      } else {
        alert(`❌ Error: ${resultado.message}`);
      }
    } catch (error) {
      alert('Error al generar token: ' + error.message);
    }
  };

  return (
    <div style={estiloContenedor}>
      {/* COLUMNA IZQUIERDA: Formulario y token */}
      <div style={estiloColumna}>
        <div style={estiloTokenPanel}>
          <strong>馃攽 Autenticaci贸n por Token</strong>
          <div style={{ marginTop: '5px', fontSize: '14px' }}>
            {tokenInfo}
          </div>
          <button 
            onClick={generarNuevoToken}
            style={{
              marginTop: '10px',
              padding: '5px 10px',
              fontSize: '12px',
              backgroundColor: '#2196F3',
              color: 'white',
              border: 'none',
              borderRadius: '3px'
            }}
          >
            馃攧 Generar Nuevo Token
          </button>
        </div>

        <h2>➕ Nueva Tarea</h2>
        <TareaForm onSubmit={manejarCrearTarea} />
        
        <div style={{ marginTop: '20px' }}>
          <button 
            onClick={cargarTareas} 
            style={{ 
              width: '100%', 
              padding: '10px',
              backgroundColor: '#4CAF50',
              color: 'white',
              border: 'none'
            }}
          >
            馃攧 Recargar Tareas
          </button>
        </div>
      </div>

      {/* COLUMNA DERECHA: Lista de tareas */}
      <div style={estiloColumna}>
        <h2>馃搵 Lista de Tareas</h2>
        
        {error && (
          <div style={{ 
            background: '#ffebee', 
            color: '#c62828', 
            padding: '10px', 
            marginBottom: '15px',
            borderRadius: '5px'
          }}>
            {error}
          </div>
        )}
        
        {cargando ? (
          <div style={{ textAlign: 'center', padding: '20px' }}>
            <p>Cargando tareas desde API...</p>
            <p style={{ fontSize: '12px', color: '#666' }}>
              (Se est谩 generando/verificando el token autom谩ticamente)
            </p>
          </div>
        ) : tareas.length === 0 ? (
          <div style={{ textAlign: 'center', padding: '20px' }}>
            <p>馃摥 No hay tareas</p>
            <p>Crea tu primera tarea usando el formulario</p>
          </div>
        ) : (
          <>
            <div style={{ 
              background: '#f5f5f5', 
              padding: '10px', 
              marginBottom: '15px',
              borderRadius: '5px'
            }}>
              <strong>✅ Conectado a la API</strong>
              <div style={{ fontSize: '14px', marginTop: '5px' }}>
                Total: {tareas.length} tareas | Token activo
              </div>
            </div>
            
            {tareas.map((tarea) => (
              <TareaItem
                key={tarea.id}
                tarea={tarea}
                onEliminar={manejarEliminarTarea}
                onCompletar={manejarCompletarTarea}
              />
            ))}
          </>
        )}
      </div>
    </div>
  );
}

export default TareaList;

✏️ Paso 8: TareaForm.js (10 minutos)

jsx
// src/components/TareaForm.js
import React, { useState } from 'react';

function TareaForm({ onSubmit }) {
  const [formData, setFormData] = useState({
    titulo: '',
    descripcion: '',
    completada: false
  });

  const [error, setError] = useState('');

  const manejarCambio = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    });
  };

  const manejarSubmit = (e) => {
    e.preventDefault();
    
    if (!formData.titulo.trim()) {
      setError('El t铆tulo es obligatorio');
      return;
    }
    
    onSubmit(formData);
    setFormData({ titulo: '', descripcion: '', completada: false });
    setError('');
  };

  return (
    <form onSubmit={manejarSubmit}>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          T铆tulo:
        </label>
        <input
          type="text"
          name="titulo"
          value={formData.titulo}
          onChange={manejarCambio}
          placeholder="Ej: Comprar leche"
          style={{ width: '100%', padding: '8px' }}
        />
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px' }}>
          Descripci贸n:
        </label>
        <textarea
          name="descripcion"
          value={formData.descripcion}
          onChange={manejarCambio}
          placeholder="Ej: Ir al supermercado"
          rows="3"
          style={{ width: '100%', padding: '8px' }}
        />
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label>
          <input
            type="checkbox"
            name="completada"
            checked={formData.completada}
            onChange={manejarCambio}
          />
          <span style={{ marginLeft: '5px' }}>Marcar como completada</span>
        </label>
      </div>
      
      <button 
        type="submit" 
        style={{ 
          width: '100%', 
          padding: '10px',
          backgroundColor: '#4CAF50',
          color: 'white',
          border: 'none'
        }}
      >
        ➕ Crear Tarea
      </button>
    </form>
  );
}

export default TareaForm;

馃摝 Paso 9: TareaItem.js (10 minutos)

jsx
// src/components/TareaItem.js
import React from 'react';

function TareaItem({ tarea, onEliminar, onCompletar }) {
  const estiloTarea = {
    border: '1px solid #ccc',
    padding: '15px',
    marginBottom: '10px',
    backgroundColor: tarea.completada ? '#e8f5e9' : '#fff'
  };

  const estiloBotones = {
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: '10px',
    marginTop: '10px'
  };

  return (
    <div style={estiloTarea}>
      <h3 style={{ marginTop: 0 }}>
        {tarea.completada ? '✅ ' : '⏳ '}
        {tarea.titulo}
      </h3>
      
      <p>{tarea.descripcion}</p>
      
      <div style={{ color: '#666', fontSize: '14px', marginBottom: '10px' }}>
        ID: {tarea.id} | 
        Estado: {tarea.completada ? 'Completada' : 'Pendiente'} | 
        Token protegido ✓
      </div>
      
      <div style={estiloBotones}>
        <button 
          onClick={() => onCompletar(tarea.id, !tarea.completada)}
          style={{
            padding: '8px',
            backgroundColor: tarea.completada ? '#ff9800' : '#4CAF50',
            color: 'white',
            border: 'none'
          }}
        >
          {tarea.completada ? '↩️ Desmarcar' : '✅ Completar'}
        </button>
        
        <button 
          onClick={() => onEliminar(tarea.id)}
          style={{
            padding: '8px',
            backgroundColor: '#f44336',
            color: 'white',
            border: 'none'
          }}
        >
          馃棏️ Eliminar
        </button>
      </div>
    </div>
  );
}

export default TareaItem;

馃幆 Paso 10: App.js mejorado (5 minutos)

jsx
// src/App.js
import React from 'react';
import TareaList from './components/TareaList';

const estiloApp = {
  padding: '20px',
  maxWidth: '1200px',
  margin: '0 auto'
};

const estiloHeader = {
  textAlign: 'center',
  marginBottom: '20px',
  paddingBottom: '15px',
  borderBottom: '2px solid #eee'
};

function App() {
  return (
    <div style={estiloApp}>
      <header style={estiloHeader}>
        <h1 style={{ color: '#333', marginBottom: '10px' }}>
          馃攼 Gestor de Tareas con Token
        </h1>
        <p style={{ color: '#666' }}>
          Conexi贸n segura a API Laravel usando autenticaci贸n por token
        </p>
        <div style={{ 
          background: '#e8f5e9', 
          padding: '10px', 
          borderRadius: '5px',
          marginTop: '10px',
          fontSize: '14px'
        }}>
          <strong>✨ Caracter铆stica:</strong> Los tokens se generan y guardan autom谩ticamente
        </div>
      </header>
      <TareaList />
    </div>
  );
}

export default App;

✅ Verificaci贸n Final

Flujo autom谩tico del token:

  1. Primera carga → Se genera token autom谩ticamente

  2. Token se guarda en localStorage

  3. Todas las peticiones incluyen el token en headers

  4. Si token es inv谩lido → Se regenera autom谩ticamente

  5. Usuario puede generar nuevo token manualmente

Estructura visual:

text
┌──────────────────────────────────────┬──────────────────────────────────────┐
│                                      │                                      │
│  馃攽 AUTENTICACI脫N POR TOKEN          │  馃搵 LISTA DE TAREAS                  │
│  Token: 123456                       │                                      │
│  [馃攧 Generar Nuevo Token]            │  ✅ Conectado a la API               │
│                                      │  Total: X tareas | Token activo      │
│  ➕ NUEVA TAREA                       │                                      │
│  [T铆tulo] _______________            │  • ✅ Comprar leche                  │
│                                      │     Ir al supermercado               │
│  [Descripci贸n] _________             │     ID: 1 | Estado: Pendiente        │
│                     ______           │     Token protegido ✓                │
│                                      │     [✅ Completar] [馃棏️ Eliminar]    │
│  [ ] Marcar como completada          │                                      │
│                                      │  • ✅ Estudiar React                 │
│  [➕ Crear Tarea]                     │     Hacer ejercicios                 │
│                                      │     ID: 2 | Estado: Completada       │
│  [馃攧 Recargar Tareas]                │     Token protegido ✓                │
│                                      │     [↩️ Desmarcar] [馃棏️ Eliminar]   │
└──────────────────────────────────────┴──────────────────────────────────────┘

馃搳 Resumen de Tiempo (75 minutos total)

PasoTiempoDescripci贸n
15 minConfiguraci贸n inicial
22 minEstructura de carpetas
31 minindex.js
42 minApp.js b谩sico
510 minTokenManager (utilidad)
615 minServicio API con token
715 minTareaList con manejo de token
810 minTareaForm
910 minTareaItem
105 minApp.js mejorado

馃攽 Caracter铆sticas del sistema de token:

Automatizaci贸n:

  • Generaci贸n autom谩tica en primera carga

  • Almacenamiento en localStorage

  • Reintento autom谩tico si token es inv谩lido

  • Header "Token" incluido en todas las peticiones

Para el usuario:

  • Panel visual del token actual

  • Bot贸n para regenerar token manualmente

  • Feedback claro de estado de conexi贸n

  • Indicador "Token protegido" en cada tarea

Para el desarrollador:

  • Clase TokenManager reutilizable

  • Manejo de errores espec铆ficos de token

  • Separaci贸n de responsabilidades clara

  • F谩cil de extender para otros proyectos

馃幆 Lo que el alumno aprendi贸:

  1. Autenticaci贸n con token en APIs REST

  2. Manejo de localStorage para persistencia

  3. Headers HTTP personalizados (Token: XXX)

  4. Recuperaci贸n autom谩tica de sesi贸n perdida

  5. Generaci贸n autom谩tica de credenciales

  6. Separaci贸n de l贸gica (servicio vs componente)

馃挕 Tips para la explicaci贸n:

  1. Mostrar localStorage en DevTools para ver el token guardado

  2. Usar Network tab para ver headers de las peticiones

  3. Simular error 401 quitando manualmente el token

  4. Demostrar la regeneraci贸n autom谩tica del token

  5. Comparar con APIs p煤blicas (sin token)

馃敡 Configuraci贸n para desarrollo:

Si hay problemas de CORS en Laravel, aseg煤rate de configurar:

bash
# Instalar paquete CORS en Laravel
composer require fruitcake/laravel-cors

# Configurar middleware en app/Http/Kernel.php
'api' => [
    \Fruitcake\Cors\HandleCors::class,
    // ... otros middlewares
],

¡Perfecto! El alumno ahora tiene una aplicaci贸n React completa que consume una API Laravel protegida por token, con manejo autom谩tico de autenticaci贸n y persistencia de sesi贸n.

Comentarios

Entradas m谩s populares de este blog

0-Sistema de Tareas

13-CRUD en Laravel para Web y API con Rutas Normales (No Resource)

10-Introducci贸n a Blade en Laravel con un Ejemplo Pr谩ctico