Getting Started in a Nutshell
Using a minimalistic setup
This guide is a step-by-step walkthrough to build a CAP application, using a minimalistic setup with Node.js and SQLite.
⓪ Preliminaries
Prerequisite: The following steps assume you've installed Node.js, Visual Studio Code, and
@sap/cds-dk
as described in the Local Setup guide.Hands-On Walkthrough: The sections below describe a hands-on walkthrough, in which you'd create a new project and fill it with content step by step.
Option: Download from GitHub – Instead of going for this hand-on step-by-step experience, you can get the final sample content from GitHub. If you choose to do so clone the repo as follows:
git clone https://github.com/sap-samples/cloud-cap-samples samples
cd samples
npm install
git clone https://github.com/sap-samples/cloud-cap-samples samples
cd samples
npm install
git clone https://github.com/sap-samples/cloud-cap-samples-java bookshop
git clone https://github.com/sap-samples/cloud-cap-samples-java bookshop
Just keep in mind that the sample code on GitHub is an already complete application showcasing a lot of features. So you might find more code in the app than in the code that is created in this step-by-step guide.
① Jumpstart a Project
- Create a new project using
cds init
cds init bookshop
cds init bookshop
cds init bookshop --add java
cds init bookshop --add java
- Open the project in VS Code
code bookshop
code bookshop
Note: VS Code CLI on macOS needs extra setup
Users on macOS must first run a command (Shell Command: Install 'code' command in PATH) to add VS Code executable to the PATH
environment variable. Read VS Code's macOS setup guide for help.
- Run
cds watch
in an Integrated Terminal
cds watch
cds watch
cd srv && mvn cds:watch
cd srv && mvn cds:watch
cds watch
is waiting for things to come...
[dev] cds w
cds serve all --with-mocks --in-memory?
live reload enabled for browsers
___________________________
No models found in db/,srv/,app/,schema,services.
Waiting for some to arrive...
[dev] cds w
cds serve all --with-mocks --in-memory?
live reload enabled for browsers
___________________________
No models found in db/,srv/,app/,schema,services.
Waiting for some to arrive...
So, let's go on adding some CDS model as follows...
② Capture Domain Models
Let's feed our project by adding a simple domain model. Start by creating a file named db/schema.cds (also indicated in the code box's label) and copy the following definitions into it:
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);
books : Association to many Books on books.author = $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;
}
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);
books : Association to many Books on books.author = $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;
}
Find this source also in cap/samples
for Node.js, and for JavaLearn more about Domain Modeling.Learn more about CDS Modeling Languages.
Deployed to Databases Automatically
As soon as you save your file, the still running cds watch
reacts immediately with new output like this:
[cds] - connect to db { database: ':memory:' } /> successfully deployed to sqlite in-memory db
This means that cds watch
detected the changes in db/schema.cds and automatically bootstrapped an in-memory SQLite database when restarting the server process.
As soon as you save your CDS file, the still running mvn cds:watch
command reacts immediately with a CDS compilation and reload of the CAP Java application. The embedded database of the started application will reflect the schema defined in your CDS file.
Compiling Models (Optional)
We can also test-compile models individually to check for validity and produce a parsed output in CSN format. For example, run this command in a new terminal:
cds db/schema.cds
cds db/schema.cds
This dumps the compiled CSN model as a plain JavaScript object to stdout.
Add --to <target>
(shortcut -2
) to produce other outputs, for example:
cds db/schema.cds -2 json
cds db/schema.cds -2 yml
cds db/schema.cds -2 sql
cds db/schema.cds -2 json
cds db/schema.cds -2 yml
cds db/schema.cds -2 sql
Learn more about the command line interface by executing cds --help
.
③ Providing Services
After the recent changes, cds watch
also prints this message:
No service definitions found in loaded models.
Waiting for some to be added...
No service definitions found in loaded models.
Waiting for some to be added...
After the recent changes, the running CAP Java application is still not exposing any service endpoints.
So, let's go on feeding it with two service definitions for different use cases:
- An AdminService for administrators to maintain Books and Authors
- A CatalogService for end users to browse and order Books
Create the following two files in folder ./srv
and fill them wih this content:
using { sap.capire.bookshop as my } from '../db/schema';
service AdminService @(requires:'authenticated-user') {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}
using { sap.capire.bookshop as my } from '../db/schema';
service AdminService @(requires:'authenticated-user') {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}
using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
@readonly entity Books as SELECT from my.Books {*,
author.name as author
} excluding { createdBy, modifiedBy };
@requires: 'authenticated-user'
action submitOrder (book: Books:ID, quantity: Integer);
}
using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
@readonly entity Books as SELECT from my.Books {*,
author.name as author
} excluding { createdBy, modifiedBy };
@requires: 'authenticated-user'
action submitOrder (book: Books:ID, quantity: Integer);
}
Find this sources also on GitHub for Node.js, and for JavaLearn more about Defining Services.
Served to OData out-of-the-box
This time cds watch
reacted with additional output like this:
[cds] - serving AdminService { at: '/admin' }
[cds] - serving CatalogService { at: '/browse', impl: 'bookshop/srv/cat-service.js' }
[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - [ terminate with ^C ]
[cds] - serving AdminService { at: '/admin' }
[cds] - serving CatalogService { at: '/browse', impl: 'bookshop/srv/cat-service.js' }
[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - [ terminate with ^C ]
As you can see in the log output, the two service definitions have been compiled and generic service providers have been constructed to serve requests on the listed endpoints /admin and /browse.
Open http://localhost:4004 in your browser and see the generic index.html page:
User
alice
is a default user with admin privileges. Use it to access theadmin
service. You don't need to enter a password.
In case the CDS service definitions were compiled correctly the Spring Boot runtime is reloaded automatically and should output a log line like this:
c.s.c.services.impl.ServiceCatalogImpl : Registered service AdminService
c.s.c.services.impl.ServiceCatalogImpl : Registered service CatalogService
c.s.c.services.impl.ServiceCatalogImpl : Registered service AdminService
c.s.c.services.impl.ServiceCatalogImpl : Registered service CatalogService
As you can see in the log output, the two service definitions have been compiled and generic service providers have been constructed to serve requests on the listed endpoints /odata/v4/AdminService and /odata/v4/browse.
WARNING
Both services defined above contain security annotations that restrict access to certain endpoints. Please add the dependency to spring-boot-security-starter to the srv/pom.xml in order to activate mock user and authentication support:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Open http://localhost:8080 in your browser and see the generic index.html page:
User
authenticated
is a prepared mock user which will be authenticated by default. Use it to access theadmin
service. You don't need to enter a password.
Compiling APIs (Optional)
You can also compile service definitions explicitly, for example to an OData model:
cds srv/cat-service.cds -2 edmx
cds srv/cat-service.cds -2 edmx
Essentially, using a CLI, this invokes what happened automatically behind the scenes in the previous steps. While we don't really need such explicit compile steps, you can do this to test correctness on the model level, for example.
④ Using Databases
Using sqlite In-Memory Database
As previously shown, cds watch
automatically bootstraps an SQLite in-process and in-memory database by default — that is, unless told otherwise. While this isn't meant for productive use, it drastically speeds up development turn-around times, essentially by mocking your target database, for example, SAP HANA.
Learn more about mocking options in Grow as you go.
Using H2 In-Memory Database
As previously shown, mvn cds:watch
automatically bootstraps an H2 in-process and in-memory database by default — that is, unless told otherwise. While this isn't meant for productive use, it drastically speeds up turn-around times in local development and furthermore allows self-contained testing.
Adding Initial Data in .csv
Files
Now, let's fill your database with initial data by adding a few plain CSV files under db/data like this:
ID,title,author_ID,stock
201,Wuthering Heights,101,12
207,Jane Eyre,107,11
251,The Raven,150,333
252,Eleonora,150,555
271,Catweazle,170,22
ID,title,author_ID,stock
201,Wuthering Heights,101,12
207,Jane Eyre,107,11
251,The Raven,150,333
252,Eleonora,150,555
271,Catweazle,170,22
ID,name
101,Emily Brontë
107,Charlotte Brontë
150,Edgar Allen Poe
170,Richard Carpenter
ID,name
101,Emily Brontë
107,Charlotte Brontë
150,Edgar Allen Poe
170,Richard Carpenter
Find a full set of .csv
files in cap/samples.
After you’ve added these files, cds watch
restarts the server with output, telling us that the files have been detected and their content been loaded into the database automatically:
[cds] - connect to db { database: ':memory:' } > filling sap.capire.bookshop.Authors from bookshop/db/data/sap.capire.bookshop-Authors.csv > filling sap.capire.bookshop.Books from bookshop/db/data/sap.capire.bookshop-Books.csv > filling sap.capire.bookshop.Books_texts from bookshop/db/data/sap.capire.bookshop-Books_texts.csv > filling sap.capire.bookshop.Genres from bookshop/db/data/sap.capire.bookshop-Genres.csv > filling sap.common.Currencies from common/data/sap.common-Currencies.csv > filling sap.common.Currencies_texts from common/data/sap.common-Currencies_texts.csv /> successfully deployed to sqlite in-memory db
This is the output when you're using the samples. It's less if you've followed the manual steps here.
After you’ve added these files, mvn cds:watch
restarts the server with output, telling us that the files have been detected and their content been loaded into the database automatically:
c.s.c.s.impl.persistence.CsvDataLoader : Filling sap.capire.bookshop.Books from db/data/sap.capire.bookshop-Authors.csv c.s.c.s.impl.persistence.CsvDataLoader : Filling sap.capire.bookshop.Books from db/data/sap.capire.bookshop-Books.csv
Learn more about Using Databases.
Querying Through OData out-of-the-box
Now that we've a connected, fully capable SQL database, filled with some initial data, we can send complex OData queries, served by the built-in generic providers:
- browse/Books?$select=ID,title
- admin/Authors?$search=Bro
- admin/Authors?$expand=books($select=ID,title)
- browse/Books?$select=ID,title
- admin/Authors?$search=Bro
- admin/Authors?$expand=books($select=ID,title)
Use Alice as user to query the
admin
service. You don't need to enter a password.
Use authenticated to query the
admin
service. You don't need to enter a password.
Learn more about Generic Providers.Learn more about OData's Query Options.
Deploying Persistent Databases
Instead of using in-memory, we can also use persistent databases. For example, still with SQLite:
npm add sqlite3 -D
cds deploy --to sqlite:my.db
npm add sqlite3 -D
cds deploy --to sqlite:my.db
The difference from the automatically provided in-memory database is that we now get a persistent database stored in the local file ./my.db. This is also recorded in the package.json. s
To see what that did, use the sqlite3 CLI with the newly created database:
sqlite3 my.db .dump
sqlite3 my.db .tables
sqlite3 my.db .dump
sqlite3 my.db .tables
You could also deploy to a provisioned SAP HANA database using this variant:
cds deploy --to hana
cds deploy --to hana
Learn more about deploying to SAP HANA.
⑤ Adding/Serving UIs
You can consume the provided services, for example, from UI frontends, using standard AJAX requests. Simply add an index.html file into the app/ folder, to replace the generic index page.
Vue.js UIs
For example, you can find a simple Vue.js app in cap/samples, which demonstrates browsing and ordering books using OData requests to the CatalogService
API we defined above.
SAP Fiori UIs
Besides, being usable from any UI frontends using standard AJAX requests, CAP provides out-of-the-box support for SAP Fiori UIs, for example, with respect to SAP Fiori annotations and advanced features such as search, value helps and SAP Fiori draft.
Learn more about Serving Fiori UIS.
Using OData Protocol
As CAP-based services are full-fledged OData services out-of-the-box, you can use advanced query options, such as $select
, $expand
, $search
, and many more.
Learn more about Serving OData Protocol.
⑥ Adding Custom Logic
While the generic providers serve most CRUD requests out-of-the-box, you can add custom code to deal with the specific domain logic of your application.
Providing Service Implementations
In Node.js, the easiest way to provide implementations for services is through equally named .js files placed next to a service definition's .cds file:
./srv
- cat-service.cds # service definitions
- cat-service.js # service implementation
...
./srv
- cat-service.cds # service definitions
- cat-service.js # service implementation
...
See these files also in cap/samples/bookshop/srv folder.Learn more about providing service implementations in Node.js.Learn also how to do that in Java using Event Handler Classes.
In CAP Java, you can add custom handlers for your service as so called EventHandlers. As CAP Java integrates with Spring Boot, you need to provide your custom code in classes, annotated with @Component
or @Service
, for example. Use your favorite Java IDE to add a class like the following to the srv/src/main/java/
folder of your application.
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogHandler implements EventHandler {
// your custom code will go here
}
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogHandler implements EventHandler {
// your custom code will go here
}
TIP
Place the code in your package of choice and use your IDE to generate the needed import
statements.
Adding Custom Event Handlers
Service implementations essentially consist of one or more event handlers.
Copy this into srv/cat-service.js to add custom event handlers:
const cds = require('@sap/cds')
module.exports = function (){
// Register your event handlers in here, for example, ...
this.after ('READ','Books', each => {
if (each.stock > 111) {
each.title += ` -- 11% discount!`
}
})
}
const cds = require('@sap/cds')
module.exports = function (){
// Register your event handlers in here, for example, ...
this.after ('READ','Books', each => {
if (each.stock > 111) {
each.title += ` -- 11% discount!`
}
})
}
Learn more about adding event handlers using <srv>.on/before/after
.
Now that you have created the classes for your custom handlers it's time to add the actual logic. You can achieve this by adding methods annotated with CAP's @Before
, @On
, or @After
to your new class. The annotation takes two arguments: the event that shall be handled and the entity name for which the event is handled.
@After(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
public void addDiscountIfApplicable(List<Books> books) {
for (Books book : books) {
if (book.getStock() > 111) {
book.setTitle(book.getTitle() + " -- 11% discount!");
}
}
}
@After(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
public void addDiscountIfApplicable(List<Books> books) {
for (Books book : books) {
if (book.getStock() > 111) {
book.setTitle(book.getTitle() + " -- 11% discount!");
}
}
}
Learn more about event handlers in the CAP Java documentation.
Consuming Other Services
Quite frequently, event handler implementations consume other services, sending requests and queries, as in the completed example below.
const cds = require('@sap/cds')
module.exports = async function (){
const db = await cds.connect.to('db') // connect to database service
const { Books } = db.entities // get reflected definitions
// Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => {
const {book,quantity} = req.data
const n = await UPDATE (Books, book)
.with ({ stock: {'-=': quantity }})
.where ({ stock: {'>=': quantity }})
n > 0 || req.error (409,`${quantity} exceeds stock for book #${book}`)
})
// Add some discount for overstocked books
this.after ('READ','Books', each => {
if (each.stock > 111) each.title += ` -- 11% discount!`
})
}
const cds = require('@sap/cds')
module.exports = async function (){
const db = await cds.connect.to('db') // connect to database service
const { Books } = db.entities // get reflected definitions
// Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => {
const {book,quantity} = req.data
const n = await UPDATE (Books, book)
.with ({ stock: {'-=': quantity }})
.where ({ stock: {'>=': quantity }})
n > 0 || req.error (409,`${quantity} exceeds stock for book #${book}`)
})
// Add some discount for overstocked books
this.after ('READ','Books', each => {
if (each.stock > 111) each.title += ` -- 11% discount!`
})
}
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class SubmitOrderHandler implements EventHandler {
private final PersistenceService persistenceService;
public SubmitOrderHandler(PersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
@On()
public void onSubmitOrder(SubmitOrderContext context) {
Select<Books_> byId = Select.from(cds.gen.catalogservice.Books_.class).byId(context.getBook());
Books book = persistenceService.run(byId).single().as(Books.class);
book.setStock(book.getStock() - context.getQuantity());
persistenceService.run(Update.entity(Books_.CDS_NAME).data(book));
context.setCompleted();
}
}
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class SubmitOrderHandler implements EventHandler {
private final PersistenceService persistenceService;
public SubmitOrderHandler(PersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
@On()
public void onSubmitOrder(SubmitOrderContext context) {
Select<Books_> byId = Select.from(cds.gen.catalogservice.Books_.class).byId(context.getBook());
Books book = persistenceService.run(byId).single().as(Books.class);
book.setStock(book.getStock() - context.getQuantity());
persistenceService.run(Update.entity(Books_.CDS_NAME).data(book));
context.setCompleted();
}
}
Find this source also in cap/samples.Find an extended version of this source also in cap/samples.Learn more about connecting to services using cds.connect
.Learn more about connecting to services using @Autowired
, com.sap.cds.ql
, etc.Learn more about reading and writing data using cds.ql
.Learn more about reading and writing data using cds.ql
.Learn more about using reflection APIs using <srv>.entities
.Learn more about typed access to data using the CAP Java SDK.
Test this implementation, for example using the Vue.js app, and see how discounts are displayed in some book titles.
Or submit orders until you see the error messages.
Summary and Next Steps
With this getting started guide we introduced many of the basics of CAP, such as:
- Jump-Starting Projects and Grow-as-you-go
- Domain Modeling
- Defining Services and APIs
- Providing Services
- Consuming Services
- Using Databases
- Adding/Serving UIs
- Adding Custom Logic
Visit our Cookbook to find more task-oriented guides. For example, you can find guides about potential next steps such as adding Authentication and Authorization or Deploying to SAP BTP, Cloud Foundry environment.
Also see the reference sections to find detailed documentation about CDS, as well as Node.js and Java Service SDKs and runtimes.