16-Tutorial Paso a Paso - Consumir API Laravel con Token en React
馃殌 Paso 1: Configuraci贸n Inicial (5 minutos)
# 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)
src/
├── components/
│ ├── TareaList.js
│ ├── TareaForm.js
│ └── TareaItem.js
├── services/
│ └── tareaService.js
├── utils/
│ └── tokenManager.js
├── App.js
└── index.js馃搫 Paso 3: index.js (1 minuto)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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:
Primera carga → Se genera token autom谩ticamente
Token se guarda en localStorage
Todas las peticiones incluyen el token en headers
Si token es inv谩lido → Se regenera autom谩ticamente
Usuario puede generar nuevo token manualmente
Estructura visual:
┌──────────────────────────────────────┬──────────────────────────────────────┐
│ │ │
│ 馃攽 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)
| Paso | Tiempo | Descripci贸n |
|---|---|---|
| 1 | 5 min | Configuraci贸n inicial |
| 2 | 2 min | Estructura de carpetas |
| 3 | 1 min | index.js |
| 4 | 2 min | App.js b谩sico |
| 5 | 10 min | TokenManager (utilidad) |
| 6 | 15 min | Servicio API con token |
| 7 | 15 min | TareaList con manejo de token |
| 8 | 10 min | TareaForm |
| 9 | 10 min | TareaItem |
| 10 | 5 min | App.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贸:
Autenticaci贸n con token en APIs REST
Manejo de localStorage para persistencia
Headers HTTP personalizados (Token: XXX)
Recuperaci贸n autom谩tica de sesi贸n perdida
Generaci贸n autom谩tica de credenciales
Separaci贸n de l贸gica (servicio vs componente)
馃挕 Tips para la explicaci贸n:
Mostrar localStorage en DevTools para ver el token guardado
Usar Network tab para ver headers de las peticiones
Simular error 401 quitando manualmente el token
Demostrar la regeneraci贸n autom谩tica del token
Comparar con APIs p煤blicas (sin token)
馃敡 Configuraci贸n para desarrollo:
Si hay problemas de CORS en Laravel, aseg煤rate de configurar:
# 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
Publicar un comentario