Juegos en Red - Grado en Diseño y Desarrollo de Videojuegos
2026-09-09
Después de implementar el juego con Phaser 3
Nota
REST para operaciones que no requieren actualización instantánea
Aplicaciones web tradicionales:
Aplicaciones con AJAX y SPA:
Petición:
GET http://www.mygame.com/players/alice
Respuesta:
{
"id": "alice",
"name": "Alice Smith",
"level": 15,
"score": 8500
}
URI (Uniform Resource Identifier)
/players/aliceURL (Uniform Resource Locator)
https://api.game.com/players/aliceEstructura de una URL:
https://api.game.com:443/players/alice?sort=score#stats
└─┬─┘ └────┬──────┘ └┬┘ └─────┬──────┘ └───┬───┘ └─┬─┘
Protocolo Dominio Puerto Ruta Query Fragment
http:// o https:// (comunicación segura)api.game.com)/players/alice)?sort=score)#stats)REST (Representational State Transfer)
Principios REST (para ser RESTful):
Tip
REST se ha convertido en el estándar de facto frente a otras alternativas como SOAP
Plain Old XML
POST /api HTTP/1.1
Body: <operation>getPlayer</operation><id>alice</id>
Advertencia
Similar a usar HTTP como túnel para otros protocolos
Introducción de recursos individuales
/players/alice y /matches/123 son URIs distintasPOST /players/alice HTTP/1.1
POST /matches/123 HTTP/1.1
Uso correcto de métodos y códigos HTTP
Importante
Este es el nivel que usaremos en este curso
GET /players/alice HTTP/1.1 → 200 OK
POST /matches HTTP/1.1 → 201 Created
DELETE /players/bob HTTP/1.1 → 204 No Content
Hypermedia as the Engine of Application State
Nota
Pocas APIs implementan HATEOAS en la práctica debido a su complejidad
Formato estándar para intercambio de datos
Estructuras principales:
Los valores pueden ser:
"Hola mundo"42, 3.14, -10true, falsenull{ "key": "value" }[1, 2, 3]Más allá de APIs REST:
En REST, todo es un recurso identificable
Ejemplos:
Recursos individuales:
/players/alice
/matches/12345
/leaderboard
Colecciones:
/players
/matches
/players/alice/scores
Reglas de buenas prácticas:
Hacer:
/players/alice ✓/players para lista/players/alice para uno/players/alice/matchesEvitar:
/getPlayer?id=alice ✗Tip
Usar minúsculas y guiones: /game-sessions/active
Operaciones CRUD sobre recursos
| Método | Operación | Seguro | Idempotente | Cacheable |
|---|---|---|---|---|
| GET | Obtener | ✓ | ✓ | ✓ |
| POST | Crear | ✗ | ✗ | ✗ |
| PUT | Actualizar | ✗ | ✓ | ✗ |
| DELETE | Eliminar | ✗ | ✓ | ✗ |
Nota
Seguro: no modifica el servidor Idempotente: múltiples peticiones = mismo resultado
Características:
Ejemplos:
GET /players/alice HTTP/1.1
→ Devuelve información del jugador alice
GET /matches HTTP/1.1
→ Devuelve lista de todas las partidas
GET /leaderboard?limit=10 HTTP/1.1
→ Devuelve top 10 del ranking
Características:
Ejemplo:
POST /matches HTTP/1.1
Content-Type: application/json
{
"type": "pong",
"mode": "ranked",
"player_id": "alice"
}
→ 201 Created
Location: /matches/67890
Características:
Ejemplo:
PUT /players/alice HTTP/1.1
Content-Type: application/json
{
"name": "Alice Smith",
"level": 16,
"score": 9000
}
→ 200 OK
Nota
PATCH se usa para actualizaciones parciales (no cubierto aquí)
Características:
Ejemplo:
DELETE /matches/12345 HTTP/1.1
→ 204 No Content
POST /matches HTTP/1.1
Host: api.game.com
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Length: 89
{
"type": "pong",
"mode": "ranked",
"max_score": 11,
"player_id": "alice"
}
Tres partes:
Cabeceras importantes:
Content-Type: application/json
Accept: application/json
Authorization: Bearer <token>
Content-Length: 89
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://api.game.com/matches/67890
Content-Length: 156
{
"id": "67890",
"type": "pong",
"mode": "ranked",
"max_score": 11,
"status": "waiting",
"created_at": "2025-01-15T14:30:00Z",
"players": ["alice"]
}
Tres partes:
Cabeceras importantes:
Content-Type: application/json
Location: http://api.game.com/matches/67890
Content-Length: 156
Números de tres dígitos que comunican el resultado
| Categoría | Significado | Uso |
|---|---|---|
| 1xx | Informativas | Raramente usados |
| 2xx | Éxito | Operación exitosa |
| 3xx | Redirecciones | Acciones adicionales |
| 4xx | Error del cliente | Error en la petición |
| 5xx | Error del servidor | Fallo del servidor |
Indican que la petición fue recibida y continúa
Nota
Veremos el código 101 cuando estudiemos WebSockets
La petición fue procesada correctamente
LocationEl cliente debe tomar acciones adicionales
LocationTip
Las redirecciones ayudan a mantener APIs retrocompatibles
Error en la petición del cliente
El servidor no pudo completar la petición
Retry-AfterAdvertencia
Los errores 5xx indican problemas del servidor, no del cliente
Framework web minimalista para Node.js
Características:
Instalación:
Habilitar módulos ES6:
Importante
"type": "module" permite usar import en lugar de require
mi-api-juego/
├── node_modules/ # Dependencias (no subir a git)
├── src/
│ ├── controllers/ # Lógica de negocio
│ ├── routes/ # Definición de rutas
│ └── app.js # Punto de entrada
├── package.json
└── package-lock.json
Importante
express.json() es fundamental para leer el body de las peticiones
Ejecutar:
Express puede servir HTML, CSS, JS del cliente
Estructura:
mi-api-juego/
├── public/
│ ├── index.html
│ ├── script.js
│ └── style.css
└── src/
└── app.js
Acceso:
http://localhost:8080/index.htmlhttp://localhost:8080/script.jsServidor simple con endpoint POST:
import express from 'express';
const app = express();
app.use(express.json());
const messages = [];
// Endpoint POST para crear un mensaje
app.post('/messages', (req, res) => {
const { text, author } = req.body;
const newMessage = {
id: Date.now(), text, author,
timestamp: new Date().toISOString()
};
messages.push(newMessage);
res.status(201).json(newMessage);
});
app.get('/messages', (req, res) => {
res.json(messages);
});
app.listen(8080, () => {
console.log('Servidor en http://localhost:8080');
});Cliente JavaScript usando POST:
async function createMessage() {
const response = await fetch('http://localhost:8080/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: 'Hola desde el cliente',
author: 'Alice'
})
});
const newMessage = await response.json();
console.log('Mensaje creado:', newMessage);
}
createMessage();Obtener los mensajes
Funciones con lógica de negocio
Patrón con closures para estado privado:
// src/controllers/anunciosController.js
const createAnunciosController = () => {
const anuncios = [];
let nextId = 1;
const getAll = (req, res) => {
res.json(anuncios);
};
const create = (req, res) => {
const { nombre, asunto, comentario } = req.body;
const nuevoAnuncio = {
id: nextId++,
nombre,
asunto,
comentario
};
anuncios.push(nuevoAnuncio);
res.status(201).json(nuevoAnuncio);
};
return { getAll, create };
};
export default createAnunciosController;Conectan URLs con controladores
// src/routes/anunciosRoutes.js
import express from 'express';
import createAnunciosController from '../controllers/anunciosController.js';
const router = express.Router();
const controller = createAnunciosController();
router.get('/', controller.getAll);
router.post('/', controller.create);
export default router;Nota
router.get('/') se convertirá en GET /anuncios con el prefijo
Resultado:
GET /anuncios → obtener todosPOST /anuncios → crear nuevoTip
:id es un parámetro dinámico disponible en req.params.id
app.post('/anuncios', (req, res) => {
const { nombre, asunto, comentario } = req.body;
// Validar datos
if (!nombre || !asunto) {
return res.status(400).json({
error: 'Nombre y asunto son requeridos'
});
}
const nuevoAnuncio = {
id: Date.now(),
nombre,
asunto,
comentario
};
anuncios.push(nuevoAnuncio);
res.status(201).json(nuevoAnuncio);
});app.put('/anuncios/:id', (req, res) => {
const { id } = req.params;
const { nombre, asunto, comentario } = req.body;
const anuncio = anuncios.find(a => a.id === parseInt(id));
if (!anuncio) {
return res.status(404).json({ error: 'No encontrado' });
}
// Actualizar propiedades
anuncio.nombre = nombre;
anuncio.asunto = asunto;
anuncio.comentario = comentario;
res.json(anuncio);
});Nota
También se puede devolver 204 (No Content) sin body
Funciones que procesan peticiones antes del controlador
Usos comunes:
Firma:
Advertencia
Olvidar llamar a next() bloquea la petición
Firma especial con 4 parámetros
Importante
Debe colocarse después de todas las rutas
API estándar moderna para peticiones HTTP
Dos formas de manejar respuestas:
Código más legible y natural
Tip
Siempre usar try/catch para manejar errores
Método por defecto de fetch()
// Obtener un jugador específico
async function getPlayer(id) {
const response = await fetch(`https://api.game.com/players/${id}`);
if (!response.ok) {
throw new Error('HTTP error ' + response.status);
}
return await response.json();
}
// Uso
const player = await getPlayer('alice');
console.log(player.name);
console.log(player.level);Nota
Template literals (`) permiten insertar variables en strings
Especificar método, headers y body
async function createPlayer(data) {
const response = await fetch('https://api.game.com/players', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('HTTP error ' + response.status);
}
return await response.json();
}
// Uso
const newPlayer = await createPlayer({
username: 'charlie',
email: 'charlie@example.com'
});
console.log('Creado con ID:', newPlayer.id);Reemplazar completamente el recurso
async function updatePlayer(id, player) {
const response = await fetch(`https://api.game.com/players/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(player)
});
if (!response.ok) {
throw new Error('HTTP error ' + response.status);
}
return await response.json();
}
// Uso: se envía el recurso completo
await updatePlayer('charlie', {
username: 'charlie',
email: 'newemail@example.com',
level: 5
});Nota
PUT reemplaza todo el recurso; PATCH actualiza solo campos específicos
Método más simple
Tip
response.ok es true para códigos 2xx
Manejo robusto con switch
async function fetchWithErrorHandling(url) {
try {
const response = await fetch(url);
if (response.ok) {
// 204 No Content no tiene body
return response.status === 204 ? null : await response.json();
}
switch (response.status) {
case 401:
throw new Error('No autenticado');
case 403:
throw new Error('Sin permisos');
case 404:
throw new Error('Recurso no encontrado');
case 500:
throw new Error('Error del servidor');
default:
throw new Error(`Error: ${response.status}`);
}
} catch (error) {
console.error('Error en petición:', error);
throw error;
}
}Componentes independientes:
API REST:
Sistema de Comandos:
Tip
Cada componente puede evolucionar independientemente
Escalar servicios según necesidades
Pruebas independientes:
La misma API sirve a múltiples clientes:
Nota
Arquitectura preparada para futuras expansiones
API REST:
JSON:
Cliente JavaScript:
fetch() con async/awaitServidor Node.js:
