Skip to Content
Technical Articles
Author's profile photo Simon Gaudek

Taking CAP to the next level – with TypeScript

Many of you may have heard about the new SAP Cloud Application Programming Model, CAP for short. It is the go-to programming model for SAP cloud-based applications. And it’s great.

CAP relies not only on Java but also on JavaScript or Node.js. Node.js is also very common in cloud development. But as the projects get bigger and bigger, the weak typing of JavaScript can be challenging. Here TypeScript comes into play to get a stronger typing. TypeScript is great and I love it.

So why not just combine these two great things together?

From this idea my colleague Michael Baudler created two modules, which allow you to combine TypeScript and CAP without any problems.

Let me introduce cds2tpes and cds-routing-handlers.
-> GitHub Demo


cds2types is a tool that creates typescript interfaces and enums from the CDS definitions. This means that the entities defined by CDS can also be used fully typed in your TypeScript code.

I just show an example using the Bookshop demo.

cds2types can be easily installed via npm or yarn:

$ npm install --save-dev cds2types

Let’s look at a CDS example:

// schema.cds
using { Currency, managed, sap } from '@sap/cds/common';
namespace sap.capire.bookshop;
entity Books : managed {
  key ID : Integer;
  title  : localized String(111);
  descr  : localized String(1111);
  author : Association to Authors;
  genre  : Association to Genres;
  stock  : Integer;
  price  : Decimal(9,2);
  currency : Currency;
entity Authors : managed {
  key ID : Integer;
  name   : String(111);
  dateOfBirth  : Date;
  dateOfDeath  : Date;
  placeOfBirth : String;
  placeOfDeath : String;
  books  : Association to many Books on = $self;
/** Hierarchically organized Code List for Genres */
entity Genres : sap.common.CodeList {
  key ID   : Integer;
  parent   : Association to Genres;
  children : Composition of many Genres on children.parent = $self;

// service.cds
using { sap.capire.bookshop as my } from './schema';
service CatalogService @(path:'/browse') {
  @readonly entity Books as SELECT from my.Books {*, as author
  } excluding { createdBy, modifiedBy };
  @requires_: 'authenticated-user'
  action submitOrder (book : Books.ID, amount: Integer);

Now when we run the CLI:

$ cds2types --cds ./service.cds --output ./service.ts --prefix I

We get the following output:

export namespace sap.capire.bookshop {
    export interface IAuthors extends IManaged {
        ID: number;
        name: string;
        dateOfBirth: Date;
        dateOfDeath: Date;
        placeOfBirth: string;
        placeOfDeath: string;
        books?: IBooks[];
    export interface IBooks extends IManaged {
        ID: number;
        title: string;
        descr: string;
        author?: IAuthors;
        author_ID?: number;
        genre?: IGenres;
        genre_ID?: number;
        stock: number;
        price: number;
        currency: unknown;
        currency_code?: string;
    export interface IGenres extends sap.common.ICodeList {
        ID: number;
        parent?: IGenres;
        parent_ID?: number;
        children: unknown;
    export enum Entity {
        Authors = "sap.capire.bookshop.Authors",
        Books = "sap.capire.bookshop.Books",
        Genres = "sap.capire.bookshop.Genres",
    export enum SanitizedEntity {
        Authors = "Authors",
        Books = "Books",
        Genres = "Genres",
export namespace CatalogService {
    export enum ActionSubmitOrder {
        name = "submitOrder",
        paramBook = "book",
        paramAmount = "amount",
    export interface IActionSubmitOrderParams {
        book: unknown;
        amount: number;
    export interface IBooks {
        createdAt?: Date;
        modifiedAt?: Date;
        ID: number;
        title: string;
        descr: string;
        author: string;
        genre?: IGenres;
        genre_ID?: number;
        stock: number;
        price: number;
        currency: unknown;
        currency_code?: string;
    export interface ICurrencies {
        name: string;
        descr: string;
        code: string;
        symbol: string;
    export interface IGenres {
        name: string;
        descr: string;
        ID: number;
        parent?: IGenres;
        parent_ID?: number;
        children: unknown;
    export enum Entity {
        Books = "CatalogService.Books",
        Currencies = "CatalogService.Currencies",
        Genres = "CatalogService.Genres",
    export enum SanitizedEntity {
        Books = "Books",
        Currencies = "Currencies",
        Genres = "Genres",

We get interfaces with all attributes of the entities and enums for all entities defined in the data model and the service definition.


Maybe you already know the routing-controllers for express.js. cds-routing-handlers is the same, only for CDS. With the cds-routing-handlers, classes can be defined as handlers for certain entities using a decorator. The methods of the class can then be defined as handlers for certain hooks of these entities.

cds-routing-handlers can also be easily installed via npm or yarn:

$ npm install cds-routing-handlers

Before, the handlers had to be implemented in files with the same name as the service definition cds file.

// service.js
const express = require("express");
function registerHandlers(srv) {
    srv.on("READ", "Entity", async () => {
        // Handle the read here...
const server = express();

But with the cds-routing-handlers, the implementation of the handlers can be spread over any number of classes.

// ./handlers/entity.handler.ts
import { Handler, OnRead, AfterRead, Entities, Req } from "cds-routing-handlers";
import { CatalogService } from "../entities": // if you are using cds2types ;-)
export class EntityHandler {
    public async read(@Req() req: any): Promise<CatalogService.IEntity[]> {
        // Handle the read here...

    public async anyOtherMethod(@Entities() data: CatalogService.IEntity[], @Req() req: any): Promise<void> {
        // Handle after read here...

And when starting the express server, these handlers only need to be referenced.

// ./server.ts
import "reflect-metadata";
import cds from "@sap/cds";
import express from "express";
import { createCombinedHandler } from "cds-routing-handlers";
import { EntityHandler } from "./handlers/entity.handler.ts";
const server = express();
const handler = createCombinedHandler({
    handler: [EntityHandler],

what next?

You all, use CAP together with TypeScript and our modules and build great applications.

A sample implementation using both modules can be found in a sample project on GitHub.

Happy Coding

Assigned Tags

      You must be Logged on to comment or reply to a post.
      Author's profile photo Henrik Damhøj Andersen
      Henrik Damhøj Andersen

      Great blog!

      Also thanks for sharing the modules.



      Author's profile photo Daniel Hutzel
      Daniel Hutzel

      Pretty cool. I also did some rudimentary steps in this direction last year. Also wired it up with our typings from `@sap/cds/api` as well as with reflected models at runtime so that you can use those generated classes as stand-ins for reflected entities and get proper code completion when writing queries using our dynamic `cds.ql`, e.g. in projections and results of statements like that.

      const author = await SELECT.from (Authors, 111, a => {
        // code completion for elements...
        a.ID,, a.books (b => { 
          // code completion for elements in nested expands...    
          b.ID, b.title
      // code completion on query results
      console.log (

      Should also work with that classes out of the box if they's fulfil the CSN entities contract.

      Note: dynamic querying is key; using static classes for data access would kill extensibility.

      Author's profile photo Simon Gaudek
      Simon Gaudek
      Blog Post Author

      Hi, Daniel,

      thank you for your comment.
      I'm pleased to hear that positive feedback is also coming from the CAP team.

      Thanks for your input.

      Kind regards

      Author's profile photo Frank Meertens
      Frank Meertens

      Hi Daniel,


      The dynamic querying is indeed key.  I was wondering if there has been any developments around this since your post?

      The projections are a powerful way to define the queries and would be great to leverage typescript for this.



      Author's profile photo Helmut Tammen
      Helmut Tammen

      Wow! Really great stuff. As a Typescript fan I absolutely like it.

      Author's profile photo Calin CRECEA
      Calin CRECEA

      Hello, I have made an integration using this blog, but I have tried to use the name of the classes instead of *.js files

      const hdl = createCombinedHandler({
            handler: [BookHandler, FunctionHandler]
      and I get the following error: "TypeError: Reflect.getMetadata is not a function"
      Any idea what could be wrong?
      (in the sample project they use 
      const hdl = createCombinedHandler({
          handler: [__dirname + "/entities/**/*.js", __dirname + "/functions/**/*.js"],
      });  (link here)
      Author's profile photo Calin CRECEA
      Calin CRECEA

      I realized I forgot to import reflect-metadata.
      Now I get another error:
      "TypeError: Cannot read property '1' of undefined

      Author's profile photo Calin CRECEA
      Calin CRECEA

      Made it working finally, the missing piece was

      "emitDecoratorMetadata": true

      in tsconfig.json

      Author's profile photo Sandeep Malhotra
      Sandeep Malhotra

      Thanks Simon for the nice blog.

      Does cds-ts watch will read the file from src folder ( src/server.ts and so on )

      If not, please let me know if any specific config I need to run using cds-ts watch

      Author's profile photo Daniel Dragolea
      Daniel Dragolea

      Hi guys,

      It appears that cds-routing-handler is not maintained anymore.

      But there's a better alternative :

      • CDS-TS-Dispatcher - TypeScript package for handling the events, support for draft
        • Example draft decorators :
          • @OnReadDraft(), @AfterReadDraft(), @OnSaveDraft()
        • Example active entity methods 
          • @OnRead(), @AfterRead(), @BeforeRead()
          • and many more ... 
      • CDS-TS-Repository - Simplified interface for common database actions
        • Example .create, createMany, update, delete, deleteMany, exists, getLocaleTexts, count, updateLocaleTexts ... and many more 

      Examples how to use the CDS-TS-Dispatcher & CDS-TS-Repository => GitHub

      Author's profile photo Tomas Rimkus
      Tomas Rimkus

      Looks awesome. Thank you very much for creating and sharing the libraries 🙂