← Volver a tutoriales

Tu primer servicio en SAP CAP con TypeScript: modelo, validaciones y campos calculados

Crea con SAP CAP un servicio OData para la biblioteca: modelo CDS, datos iniciales, validación con before y un campo virtual con after. Todo en TypeScript y probado con peticiones reales.

· Actualizado

Also available in English →

En la serie de RAP construimos la biblioteca dentro de ABAP. Ahora hacemos lo mismo fuera del sistema, con SAP Cloud Application Programming Model (CAP): el modelo de programación de SAP para servicios en BTP con Node.js o Java. Si vienes de RAP, el mapa mental es casi el mismo:

RAP CAP
Tabla + CDS View Entity Entidad en db/schema.cds
Service Definition service en srv/*.cds
Validación en el Behavior Implementation Handler before
Campo virtual con lógica virtual + handler after READ
Service Binding OData V4 Lo hace CAP solo, sin configurar nada

Requisitos

  • Node.js 22 o superior. CAP 10 no funciona con versiones anteriores.
  • Las herramientas de desarrollo de CAP, instaladas una sola vez de forma global:
npm i -g @sap/cds-dk

El paquete global trae el comando cds y una copia del runtime de CAP, que basta para desarrollar en tu equipo. En un servidor no habrá nada global, así que en el paso 1 declaramos el runtime en el propio proyecto.

1. Crear el proyecto

cds init biblioteca --add typescript,sqlite
cd biblioteca
npm install
npm install @sap/cds
npm pkg set name=biblioteca version=1.0.0
  • npm install @sap/cds declara el runtime de CAP como dependencia del proyecto. cds init no lo hace, y sin él el servicio no arrancaría al desplegarlo.
  • npm pkg set pone nombre y versión al proyecto, que cds init deja vacíos. CAP los usa como título de su página de inicio; sin ellos, sale «undefined undefined».

cds init crea la estructura estándar:

biblioteca/
├── app/        ← interfaces de usuario (no la usamos aquí)
├── db/         ← modelo de datos y datos iniciales
├── srv/        ← servicios y su lógica
├── package.json
└── tsconfig.json
  • typescript: prepara tsconfig.json, tsx y cds-typer, que genera tipos TypeScript a partir de tu modelo CDS. Este tutorial no los usa todavía; si los usas, ten en cuenta que cds watch solo los genera cuando no existen: después de cambiar un .cds, regenéralos con npx cds-typer "*".
  • sqlite: base de datos local para desarrollar, sin instalar nada más.

2. El modelo de datos

db/schema.cds:

namespace biblioteca;

using { cuid, managed } from '@sap/cds/common';

entity Authors : cuid, managed {
  name    : String(100) @mandatory;
  country : String(2);
  books   : Association to many Books on books.author = $self;
}

entity Books : cuid, managed {
  title  : String(200) @mandatory;
  pages  : Integer;
  stock  : Integer default 0;
  author : Association to Authors;
}
  • cuid añade una clave ID de tipo UUID, que CAP genera solo al crear.
  • managed añade createdAt, createdBy, modifiedAt y modifiedBy, que CAP también rellena solo. Es el equivalente de los campos de administración abp_* de RAP.
  • @mandatory hace que CAP rechace un libro sin título o un autor sin nombre, sin escribir código.
  • Las dos asociaciones forman la relación: un libro apunta a su autor y un autor ve todos sus libros (on books.author = $self).

¿Asociación o composición? Si books fuera Composition of many Books, los libros serían «parte» del autor: al borrar un autor se borrarían sus libros. En una biblioteca, un libro tiene vida propia, así que usamos Association. Reserva la composición para relaciones padre-hijo de verdad, como un pedido y sus posiciones.

3. Datos iniciales

CAP carga automáticamente los CSV de db/data/ cuyo nombre sea <namespace>-<Entidad>.csv.

db/data/biblioteca-Authors.csv:

ID,name,country
a1b2c3d4-0000-4000-8000-000000000001,Ana Rojas,CL
a1b2c3d4-0000-4000-8000-000000000002,Carlos Soto,AR
a1b2c3d4-0000-4000-8000-000000000003,Elena Vidal,ES

db/data/biblioteca-Books.csv:

ID,title,pages,stock,author_ID
b1b2c3d4-0000-4000-8000-000000000010,Aves del sur,320,4,a1b2c3d4-0000-4000-8000-000000000001
b1b2c3d4-0000-4000-8000-000000000011,Ríos de montaña,180,0,a1b2c3d4-0000-4000-8000-000000000001
b1b2c3d4-0000-4000-8000-000000000012,El último tren,410,7,a1b2c3d4-0000-4000-8000-000000000002
b1b2c3d4-0000-4000-8000-000000000013,Cuentos de invierno,120,2,a1b2c3d4-0000-4000-8000-000000000003
  • Las claves son UUID reales. Valores como 1 o 2 se cargan sin quejas en SQLite, pero no son UUID: no se parecen a los que CAP genera al crear y se mezclarían con ellos en la misma tabla. Mejor que los datos de prueba tengan la misma forma que los de verdad.
  • La asociación author se rellena con la columna author_ID: CAP guarda una asociación como clave foránea con el sufijo _ID.

4. El servicio

srv/catalog-service.cds:

using biblioteca as db from '../db/schema';

service CatalogService {
  entity Authors as projection on db.Authors;

  entity Books as projection on db.Books {
    *,
    virtual null as available : Boolean
  };
}
  • El servicio proyecta las entidades del modelo, igual que la proyección C_ de RAP: decide qué expone esta API.
  • available es un campo virtual: aparece en el servicio, pero no existe en la base de datos. Lo calcularemos en código.

5. La lógica en TypeScript

srv/catalog-service.ts. CAP lo enlaza solo porque se llama igual que el .cds:

import cds from '@sap/cds';

export default class CatalogService extends cds.ApplicationService {
  async init() {
    const { Books } = this.entities;

    // Validar antes de escribir: al crear y al modificar
    this.before(['CREATE', 'UPDATE'], Books, (req) => {
      const { stock } = req.data;
      if (stock !== undefined && stock !== null && stock < 0) {
        req.error(400, 'El stock no puede ser negativo.', 'stock');
      }
    });

    // Completar el campo virtual después de leer
    this.after('READ', Books, (books) => {
      for (const book of Array.isArray(books) ? books : [books]) {
        if (book && book.stock !== undefined) book.available = book.stock > 0;
      }
    });

    return super.init();
  }
}

Las dos fases que más usarás en CAP:

Fase Para qué Aquí
before Validar o completar la entrada antes de que llegue a la base de datos. req.error corta la petición con un error Rechazar stock negativo
after Retocar el resultado después de leer Calcular available

Tres detalles que evitan errores sutiles:

  1. ['CREATE', 'UPDATE']: validar solo al crear deja un hueco, porque con un PATCH alguien podría poner el stock en negativo.
  2. req.error(400, mensaje, 'stock'): el tercer parámetro es el campo afectado. Una app Fiori lo usa para marcar en rojo el campo correcto, igual que %element en RAP.
  3. book.stock !== undefined: si el cliente pide solo algunos campos ($select=title,available), stock no viene en el resultado. Sin esa comprobación, available saldría false aunque haya ejemplares. Así, simplemente, no se informa.

6. Ejecútalo

cds watch

cds watch detecta que el proyecto es TypeScript, lo ejecuta sin compilar, crea una base SQLite en memoria, carga los CSV y se reinicia solo cada vez que guardas un archivo. El servicio queda en http://localhost:4004/odata/v4/catalog.

7. Pruébalo

Abre http://localhost:4004 en el navegador: CAP muestra una página con enlaces a cada entidad, a su $metadata y a una vista previa Fiori generada al vuelo.

También puedes usar curl:

Libros con su disponibilidad:

curl "http://localhost:4004/odata/v4/catalog/Books?\$select=title,stock,available"

Ríos de montaña, con stock 0, sale con "available": false; los demás, con true.

Un autor con sus libros ($expand recorre la asociación):

curl "http://localhost:4004/odata/v4/catalog/Authors?\$filter=country%20eq%20'CL'&\$expand=books(\$select=title)"

Intentar crear un libro con stock negativo:

curl -X POST http://localhost:4004/odata/v4/catalog/Books \
  -H "Content-Type: application/json" \
  -d '{"title":"Prueba","stock":-1}'

Responde 400 con "message": "El stock no puede ser negativo." y "target": "stock". Lo mismo ocurre con un PATCH sobre un libro existente.

Crear un libro sin título devuelve 400 con ASSERT_MANDATORY. Eso lo hace @mandatory, sin una línea de código.

Resumen

  • db/: el modelo, con cuid y managed para no repetir claves ni campos de administración.
  • srv/*.cds: qué expone el servicio, incluidos los campos virtual.
  • srv/*.ts: la lógica. before para validar la entrada, after para completar el resultado.
  • cds watch: servidor OData V4 completo, con datos de prueba, sin configurar nada.

Siguiente paso: añadir anotaciones @UI para generar una app Fiori Elements sobre este mismo servicio, y desplegarlo en BTP con SAP HANA Cloud en lugar de SQLite.