Ir al contenido

Sesión 3 · chuleta

En el orden de las slides. Copia con el botón de cada bloque; si la slide dice «teclea», esas líneas van a mano.

📋 S3·01 · receta «Cambiar de rama» a s3-start (terminal)

Sección titulada «📋 S3·01 · receta «Cambiar de rama» a s3-start (terminal)»
Ventana de terminal
# Windows: todo en Git Bash (no PowerShell)
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)
git fetch origin
git switch -c mi-s3 origin/s3-start # checkpoint: mi-s3-cp origin/s3-checkpoint
npm install
npx prisma generate
npx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed

📋 S3·02 · instalar Express 5 y Zod (terminal)

Sección titulada «📋 S3·02 · instalar Express 5 y Zod (terminal)»
Ventana de terminal
npm install express zod
npm install -D @types/express

📋 S3·03 · scripts dev y start (package.json, al principio de “scripts”)

Sección titulada «📋 S3·03 · scripts dev y start (package.json, al principio de “scripts”)»
"dev": "tsx watch src/server.ts",
"start": "tsx src/server.ts",
src/server.ts
import { app } from './app.js';
const PORT = Number(process.env.PORT ?? 3000);
app.listen(PORT, () => {
console.log(`AcademIA API escuchando en http://localhost:${PORT}`);
});
src/app.ts
import express from 'express';
import { coursesRouter } from './routes/courses.routes.js';
// La app se exporta sin arrancar: server.ts hace listen() y los tests
// (sesión 6) la usan directamente con Supertest.
export const app = express();
app.use(express.json());
app.use('/api/v1/courses', coursesRouter);

📋 S3·06 · courses.repository.ts completo (de momento)

Sección titulada «📋 S3·06 · courses.repository.ts completo (de momento)»
// src/repositories/courses.repository.ts (de momento, solo findMany)
import { prisma } from '../db.js';
export const coursesRepository = {
findMany: (params: {
skip: number; take: number; categoryId?: number;
}) =>
prisma.course.findMany({
where: { categoryId: params.categoryId, published: true },
skip: params.skip, take: params.take,
// sin orden explícito, la paginación no es determinista
orderBy: { id: 'asc' },
include: { category: true },
}),
};

📋 S3·07 · course.schema.ts (de momento, solo el del listado)

Sección titulada «📋 S3·07 · course.schema.ts (de momento, solo el del listado)»
// src/schemas/course.schema.ts (de momento, solo el del listado)
import { z } from 'zod';
export const listCoursesQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
pageSize: z.coerce.number().int().positive().max(50).default(10),
categoryId: z.coerce.number().int().positive().optional(),
});
export type ListCoursesQuery = z.infer<typeof listCoursesQuerySchema>;

📋 S3·08 · courses.service.ts completo (de momento)

Sección titulada «📋 S3·08 · courses.service.ts completo (de momento)»
src/services/courses.service.ts
import { coursesRepository } from '../repositories/courses.repository.js';
import type { ListCoursesQuery } from '../schemas/course.schema.js';
export const coursesService = {
listCourses: ({ page, pageSize, categoryId }: ListCoursesQuery) =>
coursesRepository.findMany({
skip: (page - 1) * pageSize, take: pageSize, categoryId,
}),
};

📋 S3·09 · courses.controller.ts (de momento, solo list)

Sección titulada «📋 S3·09 · courses.controller.ts (de momento, solo list)»
src/controllers/courses.controller.ts
import type { Request, Response } from 'express';
import { coursesService } from '../services/courses.service.js';
import { listCoursesQuerySchema } from '../schemas/course.schema.js';
// Express 5: si un handler async lanza (o su promesa se rechaza), el error
// llega solo al errorHandler. No hace falta try/catch ni asyncHandler.
export const coursesController = {
list: async (req: Request, res: Response) => {
const query = listCoursesQuerySchema.parse(req.query); // ZodError → 400
res.status(200).json(await coursesService.listCourses(query));
},
};

📋 S3·10 · courses.routes.ts completo (de momento)

Sección titulada «📋 S3·10 · courses.routes.ts completo (de momento)»
src/routes/courses.routes.ts
import { Router } from 'express';
import { coursesController } from '../controllers/courses.controller.js';
export const coursesRouter = Router();
coursesRouter.get('/', coursesController.list);
Ventana de terminal
export API=http://localhost:3000/api/v1 # guarda la URL; $API la usa
curl -i "$API/courses?page=abc" # -i: status y cabeceras
curl -X POST "$API/courses/4/enrollments" \
-H "Content-Type: application/json" \
-d '{"userId":1}' # -X método, -H cabecera, -d body
curl -s -o /dev/null -w "%{http_code}\n" "$API/courses" # solo el código
curl -s -D - -o /dev/null "$API/courses" # solo las cabeceras

📋 S3·12 · otro puerto y la variable API (terminal)

Sección titulada «📋 S3·12 · otro puerto y la variable API (terminal)»
Ventana de terminal
PORT=3001 npm run dev # solo si el 3000 está ocupado
export API=http://localhost:3001/api/v1 # export: si no, los checks no la ven
curl "$API/courses" # todos los curl, con $API
npx tsx checks/s3.ts # lee API (por defecto, el 3000)

📋 S3·13 · arrancar la API y probar el listado (terminal)

Sección titulada «📋 S3·13 · arrancar la API y probar el listado (terminal)»
Ventana de terminal
npm run dev # terminal 1: déjala abierta
# terminal 2 (Windows: Git Bash)
export API=http://localhost:3000/api/v1 # tu puerto, si no es el 3000
curl "$API/courses?page=1&pageSize=5"
curl -i "$API/courses?page=abc" # → 500

📋 S3·14 · createCourseSchema (añade en course.schema.ts)

Sección titulada «📋 S3·14 · createCourseSchema (añade en course.schema.ts)»
// añade en src/schemas/course.schema.ts (el import de z ya está)
export const createCourseSchema = z.object({
title: z.string().min(3).max(120),
description: z.string().max(2000).optional(),
price: z.number().positive(),
level: z.enum(['BEGINNER', 'INTERMEDIATE', 'ADVANCED']).optional(),
published: z.boolean().optional(),
maxSeats: z.number().int().positive().optional(),
categoryId: z.number().int().positive().optional(),
// ⚠ Provisional: en la sesión 4 se toma de req.user
instructorId: z.number().int().positive(),
});

📋 S3·15 · idParamSchema y CreateCourseInput (añade en course.schema.ts)

Sección titulada «📋 S3·15 · idParamSchema y CreateCourseInput (añade en course.schema.ts)»
// añade en src/schemas/course.schema.ts
export const idParamSchema = z.object({
id: z.coerce.number().int().positive(),
});
export type CreateCourseInput = z.infer<typeof createCourseSchema>;
src/middlewares/validate.ts
import type { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
// Valida y sustituye req.body. (En Express 5 req.query es de solo lectura:
// query y params se validan con schema.parse() dentro del controlador.)
export const validateBody = (schema: z.ZodType) =>
(req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
res.status(400).json({
error: 'Validation failed',
details: z.flattenError(result.error),
});
return;
}
req.body = result.data;
next();
};

📋 S3·17 · mover errors.ts a errors/app-error.ts (terminal)

Sección titulada «📋 S3·17 · mover errors.ts a errors/app-error.ts (terminal)»
Ventana de terminal
mkdir src/errors
git mv src/errors.ts src/errors/app-error.ts
// src/errors/app-error.ts (antes src/errors.ts)
// Errores de dominio con su código HTTP. El errorHandler los traduce a respuestas.
export class AppError extends Error {
constructor(public readonly statusCode: number, message: string) {
super(message);
this.name = new.target.name;
}
}
export class NotFoundError extends AppError {
// CAMBIA: antes (message = 'Not found'); ahora recibe el recurso
constructor(resource: string) { super(404, `${resource} not found`); }
}
export class ConflictError extends AppError {
constructor(message: string) { super(409, message); }
}
// UnauthorizedError y ForbiddenError: se usan en la sesión 4
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') { super(401, message); }
}
export class ForbiddenError extends AppError {
constructor(message = 'Forbidden') { super(403, message); }
}
src/middlewares/error-handler.ts
import type { NextFunction, Request, Response } from 'express';
import { z } from 'zod';
import { Prisma } from '../../generated/prisma/client.js'; // no de '@prisma/client'
import { AppError } from '../errors/app-error.js';
// Middleware de error: Express lo reconoce por tener 4 parámetros.
// Siempre el último app.use().
export function errorHandler(
err: unknown, req: Request, res: Response, _next: NextFunction,
) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err.message });
}
if (err instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed', details: z.flattenError(err),
});
}
// JSON mal formado en el body (lo lanza express.json())
if ((err as { type?: string }).type === 'entity.parse.failed') {
return res.status(400).json({ error: 'Malformed JSON' });
}
if (err instanceof Prisma.PrismaClientKnownRequestError) {
switch (err.code) {
case 'P2002': return res.status(409).json({ error: 'Resource already exists' });
case 'P2025': return res.status(404).json({ error: 'Resource not found' });
// FK: borrar algo que otros registros referencian, o crear con un id relacionado que no existe
case 'P2003': return res.status(409).json({ error:
'Foreign key constraint failed (related resource missing or still referenced)' });
}
}
console.error(err); // en producción: un logger de verdad
return res.status(500).json({ error: 'Internal server error' });
}

📋 S3·20 · enchufar el errorHandler (añade en app.ts)

Sección titulada «📋 S3·20 · enchufar el errorHandler (añade en app.ts)»
// src/app.ts: el import arriba, con los demás…
import { errorHandler } from './middlewares/error-handler.js';
// …y al final del fichero, después de todas las rutas
app.use(errorHandler); // siempre el último

📋 S3·21 · ?page=abc y ?pageSize=500 → 400 (terminal)

Sección titulada «📋 S3·21 · ?page=abc y ?pageSize=500 → 400 (terminal)»
Ventana de terminal
curl -i "$API/courses?page=abc" # → 400
curl -i "$API/courses?pageSize=500" # → 400

📋 S3·22 · traer solo lo nuevo del punto de control (terminal)

Sección titulada «📋 S3·22 · traer solo lo nuevo del punto de control (terminal)»
Ventana de terminal
git fetch origin
git checkout origin/s3-checkpoint -- checks src/app.ts src/routes src/controllers/enrollments.controller.ts src/schemas/enrollment.schema.ts

📋 S3·23 · receta «Cambiar de rama» al punto de control (terminal)

Sección titulada «📋 S3·23 · receta «Cambiar de rama» al punto de control (terminal)»
Ventana de terminal
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)
git fetch origin
git switch -c mi-s3-cp origin/s3-checkpoint # mi-s3 ya existe: otro nombre
npm install
npx prisma generate
npx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed

📋 S3·24 · comprobación de la práctica (terminal)

Sección titulada «📋 S3·24 · comprobación de la práctica (terminal)»
Ventana de terminal
npx tsx checks/s3.ts

📋 S3·25 · código de la IA para auditar (s3-repo-servicio.ia.ts, solo para leer)

Sección titulada «📋 S3·25 · código de la IA para auditar (s3-repo-servicio.ia.ts, solo para leer)»
// ─────────────────────────────────────────────────────────────────────────
// Generado por un asistente de IA · Sesión 3
// Prompt: "Genera el repositorio y el servicio de courses para mi API Express
// con Prisma: listar paginado, obtener por id, crear, editar, borrar"
// Tu tarea: encuentra lo que falta o está mal. Pista: hay al menos 5 cosas.
// ─────────────────────────────────────────────────────────────────────────
import type { Request, Response } from 'express';
import { prisma } from '../db.js';
export const coursesRepository = {
findMany: (page: number, pageSize: number) =>
prisma.course.findMany({ skip: (page - 1) * pageSize, take: pageSize }),
findById: (id: number) => prisma.course.findUniqueOrThrow({ where: { id } }),
create: (data: any) => prisma.course.create({ data }),
update: (id: number, data: any) => prisma.course.update({ where: { id }, data }),
delete: (id: number) => prisma.course.delete({ where: { id } }),
};
export const coursesController = {
list: async (req: Request, res: Response) => {
const page = Number(req.query.page) || 1;
const pageSize = Number(req.query.pageSize) || 10;
res.json(await coursesRepository.findMany(page, pageSize));
},
get: async (req: Request, res: Response) => {
res.json(await coursesRepository.findById(Number(req.params.id)));
},
create: async (req: Request, res: Response) => {
res.json(await coursesRepository.create(req.body));
},
remove: async (req: Request, res: Response) => {
await coursesRepository.delete(Number(req.params.id));
res.json({ ok: true });
},
};