Create a CAP Business Service with Node.js Using Visual Studio Code
- How to develop a sample business service using CAP and
Node.js
- How to define a simple data model and a service that exposes the entities you created in your data model
- How to run your service locally
- How to deploy the data model to an
SQLite
database - How to add custom handlers to serve requests that aren’t handled automatically
Prerequisites
- You’ve installed Node.js. Make sure you run the latest long-term support (LTS) version of Node.js with an even number like 18. Refrain from using odd versions, for which some modules with native parts will have no support and thus might even fail to install. In case of problems, see the Troubleshooting guide for CAP.
- You’ve installed the latest version of Visual Studio Code (VS Code).
- (Windows only) You’ve installed the SQLite tools for Windows. Find the steps how to install it in the How Do I Install SQLite section of the CAP documentation.
- You’ve installed an HTTP client, for example, REST client.
- If you don’t have a Cloud Foundry Trial subaccount and dev space on SAP BTP yet, create your Cloud Foundry Trial Account with US East (VA) as region and, if necessary Manage Entitlements. You need this to continue after this tutorial.
Before you start, make sure that you’ve completed the prerequisites.
- Step 1
-
Open a command line window and install the
cds
development kit globally by executing the following command:Shell/BashCopynpm i -g @sap/cds-dk
This process takes some minutes installing the
cds
command, which you will use in the next steps.On MacOS/Linux, you need to follow the steps as described here.
If there’s an older
@sap/cds
package already installed on your machine, you have to remove it first; if so, you’ll be instructed to do so.In case of problems, see the Troubleshooting guide in the CAP documentation for more details.
-
To verify that the installation was successful, run
cds
without arguments:Shell/BashCopycds
This lists the available
cds
commands. For example, usecds --version
to check the version that you’ve installed. To know what is the latest version, see the Release Notes for CAP.
-
- Step 2
-
Go to Visual Studio Marketplace.
-
Choose Install.
VS Code opens the extensions details page.
-
In VS Code choose Install to enable the extension for SAP CDS Language Support.
If the extension is already installed and enabled in VS Code, it will be updated automatically.
Learn more about the features in this short demo and see the features and commands in the CAP documentation.
-
- Step 3
- Step 4
After initializing the project, you should see the following empty folders:
app
: for UI artifactsdb
: for the database level schema modelsrv
: for the service definition layer
-
Let’s feed it by adding a simple domain model. In the
srv
folder choose the New File icon in VS Code and create a new file calledcat-service.cds
. -
Add the following code to the file
cat-service.cds
:CDSCopyusing { Country, managed } from '@sap/cds/common'; service CatalogService { entity Books { key ID : Integer; title : localized String; author : Association to Authors; stock : Integer; } entity Authors { key ID : Integer; name : String; books : Association to many Books on books.author = $self; } entity Orders : managed { key ID : UUID; book : Association to Books; country : Country; amount : Integer; } }
Remember to save your files choosing Ctrl + S.
-
As soon as you’ve saved your file, the still running
cds watch
reacts immediately with some new output as shown below:Shell/BashCopy[cds] - connect using bindings from: { registry: '~/.cds-services.json' } [cds] - connect to db > sqlite { database: ':memory:' } > init from ./db/csv/my.bookshop-Authors.csv /> successfully deployed to sqlite in-memory db [cds] - serving CatalogService { at: '/odata/v4/catalog', impl: './srv/cat-service.js' } [cds] - server listening on { url: 'http://localhost:4004' } [cds] - launched at 18/05/2022, 19:49:32, in: 874.456ms [cds] - [ terminate with ^C ]
This means,
cds watch
detected the changes insrv/cat-service.cds
and automatically bootstrapped an in-memory SQLite database when restarting the server process. -
To test your service, go to: http://localhost:4004
You won’t see data, because you haven’t added a data model yet. Click on the available links to see the service is running.
- Step 5
Add service provider logic to return mock data.
-
In the
srv
folder, create a new file calledcat-service.js
. -
Add the following code to the file
cat-service.js
:JavaScriptCopymodule.exports = (srv) => { // Reply mock data for Books... srv.on ('READ', 'Books', ()=>[ { ID:201, title:'Wuthering Heights', author_ID:101, stock:12 }, { ID:251, title:'The Raven', author_ID:150, stock:333 }, { ID:252, title:'Eleonora', author_ID:150, stock:555 }, { ID:271, title:'Catweazle', author_ID:170, stock:222 }, ]) // Reply mock data for Authors... srv.on ('READ', 'Authors', ()=>[ { ID:101, name:'Emily Brontë' }, { ID:150, name:'Edgar Allen Poe' }, { ID:170, name:'Richard Carpenter' }, ]) }
Remember to save your files choosing Ctrl + S.
-
To test your service, click on these links:
You should see the mock data that you’ve added for the
Books
andAuthors
entities.
-
- Step 6
To get started quickly, you’ve already added a simplistic all-in-one service definition. However, you would usually put normalized entity definitions into a separate data model and have your services expose potentially de-normalized views on those entities.
-
In the
db
folder choose the New File icon in VS Code and create a new file calleddata-model.cds
. -
Add the following code to the file
data-model.cds
:CDSCopynamespace my.bookshop; using { Country, managed } from '@sap/cds/common'; entity Books { key ID : Integer; title : localized String; author : Association to Authors; stock : Integer; } entity Authors { key ID : Integer; name : String; books : Association to many Books on books.author = $self; } entity Orders : managed { key ID : UUID; book : Association to Books; country : Country; amount : Integer; }
-
Open the file
cat-service.cds
and replace the existing code with:CDSCopyusing my.bookshop as my from '../db/data-model'; service CatalogService { entity Books @readonly as projection on my.Books; entity Authors @readonly as projection on my.Authors; entity Orders @insertonly as projection on my.Orders; }
Remember to save your files choosing Ctrl + S.
-
- Step 7
In VS Code you will add plain CSV files in folder
db/csv
to fill your database tables with initial data.-
In the
db
folder, choose New File and entercsv/my.bookshop-Authors.csv
to create a new foldercsv
with the file namedmy.bookshop-Authors.csv
. Add the following to the file:CSVCopyID;name 101;Emily Brontë 107;Charlote Brontë 150;Edgar Allen Poe 170;Richard Carpenter
-
In the newly created
csv
folder, choose New File and create a file calledmy.bookshop-Books.csv
. Add the following to the file:CSVCopyID;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
Remember to save your files choosing Ctrl + S.
Make sure that you now have a folder hierarchy
db/csv/...
. Remember that thecsv
files must be named like the entities in your data model and must be located inside thedb/csv
folder.After you added these files,
cds watch
restarts the server with an output, telling that the files have been detected and their content been loaded into the database automatically:Shell/BashCopy[cds] - connect using bindings from: { registry: '~/.cds-services.json' } [cds] - connect to db > sqlite { database: ':memory:' } > init from ./db/csv/my.bookshop-Books.csv /> successfully deployed to sqlite in-memory db [cds] - serving CatalogService { at: '/odata/v4/catalog', impl: './srv/cat-service.js' } [cds] - server listening on { url: 'http://localhost:4004' } [cds] - launched at 18/05/2022, 19:49:47, in: 861.036ms [cds] - [ terminate with ^C ]
-
Remove the code with mock data in
cat-service.js
, because you want to see the data loaded from thecsv
files. -
To test your service, open a web browser and go to:
http://localhost:4004/odata/v4/catalog/Books
http://localhost:4004/odata/v4/catalog/Authors
As you now have a fully capable SQL database with some initial data, you can send complex OData queries, served by the built-in generic providers.
http://localhost:4004/odata/v4/catalog/Authors?$expand=books($select=ID,title)
You should see a book titled Jane Eyre. If not, make sure you’ve removed the mock data from
cat-service.js
.
-
- Step 8
Before you continue, make sure that you’ve completed the prerequisites and installed SQLite (for Windows users only).
Instead of using in-memory, you can also use persistent databases.
-
If
cds watch
is running, choose Ctrl + C in the command line to stop the service. -
Install
SQLite3
packages.Shell/BashCopynpm i sqlite3 -D
-
Deploy the data model to an
SQLite
database:Shell/BashCopycds deploy --to sqlite:db/my-bookshop.db
You’ve now created an
SQLite
database file underdb/my-bookshop.db
.This configuration is saved in your
package.json
as your default data source. For subsequent deployments using the default configuration, you just need to runcds deploy
.The difference to the automatically provided in-memory database is that you now get a persistent database stored in the local file.
-
Open
SQLite
and view the newly created database:Shell/BashCopysqlite3 db/my-bookshop.db -cmd .dump
If this doesn’t work, check if you have SQLite installed. On Windows, you might need to enter the full path to SQLite, for example:
C:\sqlite\sqlite3 db/my-bookshop.db -cmd .dump
. Find the steps how to install it in the Troubleshooting guide in section How Do I Install SQLite in the CAP documentation for more details. -
To stop
SQLite
and go back to your project directory, choose Ctrl + C. -
Run your service.
Shell/BashCopycds watch
Shell/BashCopy[cds] - connect using bindings from: { registry: '~/.cds-services.json' } [cds] - connect to db > sqlite { url: 'sqlite.db', database: 'db/my-bookshop.db' } [cds] - serving CatalogService { at: '/odata/v4/catalog', impl: './srv/cat-service.js' } [cds] - server listening on { url: 'http://localhost:4004' } [cds] - launched at 18/05/2022, 19:54:53, in: 830.398ms [cds] - [ terminate with ^C ]
-
- Step 9
You can now see the generic handlers shipped with CAP in action.
In the root of your project, create a file called
test.http
and copy the following requests in it. This file can be used with the REST client to make requests against your service. The generic handlers CAP provides sent the responses to your requests.HTTPCopy### # # Browse Books # GET http://localhost:4004/odata/v4/catalog/Books? # &$select=title,stock # &$expand=currency # &sap-language=de ### # # Get Author wit ID 101 # GET http://localhost:4004/odata/v4/catalog/Authors(101) ### # # Update Author with ID 101 # POST http://localhost:4004/odata/v4/catalog/Authors Content-Type: application/json {"ID": 101, "name": "Some Author"} ### # # Order a Book # POST http://localhost:4004/odata/v4/catalog/Orders Content-Type: application/json;IEEE754Compatible=true {"book_ID": 201, "amount": 5}
Click on
Send Request
inside thetest.http
file, to execute requests against your service.This
Send Request
button is provided by the REST client. It appears for every single request. This is important for the following step, when you execute, for example, theOrder a Book
request.The REST client gives you the response of your service and you see immediately if the request was successful.
Use the following request to answer the next question:
HTTPCopyPOST http://localhost:4004/odata/v4/catalog/Books Content-Type: application/json {"ID": 201, "title": "Some Title"}
You can only GET books and authors, but modifications won't work. Why is that? The response to your request helps you answer this question.
- Step 10
-
In VS Code open the file
cat-service.js
and replace the existing code with:JavaScriptCopymodule.exports = (srv) => { const {Books} = cds.entities ('my.bookshop') // Reduce stock of ordered books srv.before ('CREATE', 'Orders', async (req) => { const order = req.data if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book') const tx = cds.transaction(req) const affectedRows = await tx.run ( UPDATE (Books) .set ({ stock: {'-=': order.amount}}) .where ({ stock: {'>=': order.amount},/*and*/ ID: order.book_ID}) ) if (affectedRows === 0) req.error (409, "Sold out, sorry") }) // Add some discount for overstocked books srv.after ('READ', 'Books', each => { if (each.stock > 111) each.title += ' -- 11% discount!' }) }
Remember to save your files choosing Ctrl + S.
Whenever orders are created, this code is triggered. It updates the book stock by the given amount, unless there aren’t enough books left.
-
In the
test.http
file, execute theBrowse Books
request.Look at the stock of book
201
. -
Execute the
Order a Book
request.This triggers the logic above and reduces the stock.
-
Execute the
Browse Books
request again.The stock of book
201
is lower than before. In the response you also see, that overstocked books get a discount now.
-