Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
thomas_jung
Developer Advocate
Developer Advocate

Introduction


I was recently working on a project where I was building the SAP Cloud Application Programming Model back-end and service layers. It would be Node.js and HANA based.  However the UI front-end would be built by a team of developers with no prior SAP or HANA experience. In our first meeting, this other development team immediately expressed concern that they wouldn't know how to call the backend because "they didn't know SAP stuff."

I tried to calm any fears by ensuring them everything would be quite standard as in OData V4 and REST service based interfaces.  No real SAP technology knowledge needed just to consume the services.  But still there was apprehension.

Ultimately what we needed was a simple UI where they could explore all the APIs exposed by the backend.  This would be a place where they could view all the input and output parameters, documentation and ideally even test the services. And so began some experimentation with openAPI/Swagger began which I'd like to share with you in this blog.

From API Hub to Swagger


If what I've described so far sounds a lot like the SAP API Business Hub to you; you'd not be very far off base. In fact the experience a developer gets from the SAP API Business Hub is precisely what we were looking for.  It has the functionality to search for particular service endpoints, explore the parameters of a service and test the service all from a web user interface.

If you've never experienced the SAP API Business Hub, here is a sample of what it looks like:



 

So we'd found the experience that we needed to help our UI development team, but we needed to keep our API data private and self contained within the service itself.  This wasn't an SAP product to publish public APIs so we couldn't just use the SAP API Business Hub. We also looked at SAP Cloud Platform API Management, but we also weren't needed full blown API management for this small scale project.  We really just wanted a very small scale documentation feature added to our service and I only wanted to spend an hour or so getting it all in place. Time was of the essence in this project.

Some research into the SAP API Business Hub revealed that all of the UI experience is based upon some open standards and reusable concepts that we might be able to tap into directly.  At the heart of things we have the openAPI (formerly known as Swagger) standard:

https://swagger.io/docs/specification/about/

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md

This is standardized specification for describing services and their interfaces.

The other half of the equation is something to visualize this specification into the interactive UI. There we looked to Swagger UI as the starting point:

https://swagger.io/tools/swagger-ui/

https://github.com/swagger-api/swagger-ui

Utilizing these open standards and projects we had all the pieces we needed to build our own service specific API documentation site with minimal code and effort.

Here is a look at what we ended up being able to provide out of our CAP based service:





For the remainder of this blog, I'd like to describe how we bridged from our existing services (based upon CAP and custom Node.js Express REST endpoints), exposed them as openAPI and integrated the SwaggerUI into our service.  Please note that I'm going to utilize several open source Node.js modules as part of this effort. These are NOT supplied, supported or directly endorsed by SAP.  These are all projects and modules that I found on my own and I'm sharing my experiences here, but you should of course do your own due diligence before using any open source content in your own applications.

Node.js Express Handlers for the Swagger Endpoints


Let's begin by looking at how to hook the Swagger endpoints into a Node.js/CAP based service. I have a service module in my MTA project that mixes both custom REST endpoints with Cloud Application Programming Model OData endpoints.  I'm using the CAP CDS module as just another Express middelware alongside other middelwares and my own Express handlers.

Therefore I started the SwaggerUI integration by just creating another Express Route Handler:


module.exports = async (app) => {
const swaggerUi = require('swagger-ui-express')
const swaggerSpec = await app.swagger.getOpenAPI()

app.get('/api/api-docs.json', function (req, res) {
res.setHeader('Content-Type', 'application/json')
res.send(swaggerSpec)
})
let options = {
explorer: true
}
app.use('/api/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, options))
}

There are two additional endpoints we are adding to Express here in this logic via the app.get and app.use calls.

The first one (app.get) is going to expose an api-docs.json endpoint which is the openAPI specification document itself. You see we are generating this JSON object with the specification details via a call to app.swagger.getOpenAPI.  This is a function we wrote which we will look at in more detail in a moment.

This api-docs.json could then be exposed to external tools or another Swagger-UI website to render the interactive view of the service interfaces.  However we wanted to keep the Swagger-UI all local to our service well so that everything is self-contained. This is where the open source module - swagger-ui-express - becomes very handy.

swagger-ui-express:

https://www.npmjs.com/package/swagger-ui-express

https://github.com/scottie1984/swagger-ui-express

This module is a helpful wrapper around the official Swagger UI Distribution (https://www.npmjs.com/package/swagger-ui-dist) that just makes it easy to directly integrate this with an Express server. It was perfect for our needs because with the app.use line above you can see how easy it was to get the entire Swagger-UI site up and running from our existing service.

openAPI For Our Custom REST Endpoints


Now comes the fun part. What exactly are we doing in that getOpenAPI function to generate the specification document for our services.  We don't want to hard code any of the service specifications in a separate document that might become out of sync with our services.  Ideally we want this specification to be as closely aligned with our services as possible so that this is a "live" API documentation.

For our custom REST endpoints this is a bit more challenging as there is no metadata description of them.  This is where another open source Node.js module from the community is helpful. We decided to use the swagger-jsdoc module to convert JavaScript Documentation from within our source code directly in to the openAPI JSON output.

swagger-jsdoc:

https://www.npmjs.com/package/swagger-jsdoc

https://github.com/Surnet/swagger-jsdoc

The first half of the getOpenAPI function in our code uses this module to scan all the JavaScript files in the routes folder of our project and dynamically build the openAPI specification from the documentation in these files.
	this.getOpenAPI = async() => {
let swaggerJSDoc = require('swagger-jsdoc')

var options = {
swaggerDefinition: {
openapi: '3.0.0',
info: {
title: 'SAP Team Task',
version: '1.0.0',
"x-odata-version": '4.0'
},
tags: [{
name: "Team Task"
}],
},
apis: ['./routes/*']
}
var swaggerSpec = swaggerJSDoc(options)

Then we just need to add a JSDoc compliant comment block with the @swagger tag at the beginning with the basic service definition for each endpoint. For example this very simple endpoint that we used to just test the security setup:
	/**
* @swagger
*
* /srv_api/admin:
* get:
* summary: Test Admin Endpoint
* tags:
* - Admin
* responses:
* '200':
* description: Admin Features
*/
app.get('/admin', function (req, res) {
if (req.authInfo !== undefined && req.authInfo.checkLocalScope("Admin")) {
return res.send('Admin Features')
} else {
return res.type("text/plain").status(401).send(`ERROR: Not Authorized. Missing Admin scope`)
}
})

Of course this documentation can get much more complex for service endpoints with more advanced parameters.  You can define schema for array/table input and output parameters for example:
	/**
* @swagger
*
* components:
* schemas:
* Table:
* type: object
* properties:
* TABLE_NAME:
* type: string
*/

/**
* @swagger
*
* /srv_api/admin/tables:
* get:
* summary: Get a list of all tables in the local container
* tags:
* - Admin
* responses:
* '200':
* description: A List of all tables
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Table'
* '401':
* description: ERROR Not Authorized. Missing Admin scope
* '500':
* description: General DB Error
*/
app.get("/admin/tables", async (req, res) => {

 

openAPI for our CDS based OData Service Endpoints


The REST endpoints turned out to be the hard part of this process. For the CAP/OData service endpoints, we already had the necessary schema and metadata definition within the CDS definition of the service.  We only needed a way to convert this to the Swagger/openAPI format.  The CAP @Sap/cds module already contains lots of functionality to compile internal specification (CSN or Core Schema Notation - https://cap.cloud.sap/docs/cds/csn) to other formats.  Someday perhaps it will even support direct compile to Swagger/openAPI.  But until then we can once again use an open source community module to help us out.

The approach here is to use the cds.compile feature to first convert to OData edmx format. From that point we can use the open source module, odata2openapi, to convert us the rest of the way to Swagger/openAPI.

odata2openapi:

https://www.npmjs.com/package/odata2openapi

https://github.com/elasticio/odata2openapi

And here is the code that makes up the second half of the getOpenAPI function in our code:
		const odataOptions = {}
try {
const {
parse,
convert
} = require('odata2openapi');
const cds = require("@sap/cds")
const csn = await cds.load([global.__base + "/gen/csn.json"])
let metadata = cds.compile.to.edmx(csn, {
version: 'v4',
})

let service = await parse(metadata)
let swagger = await convert(service.entitySets, odataOptions, service.version)
Object.keys(swagger.paths).forEach(function (key) {
let val = swagger.paths[key]
swaggerSpec.paths[`/srv_api/odata/v4/teamtask${key}`] = val
})
swaggerSpec.definitions = swagger.definitions
return swaggerSpec
} catch (error) {
app.logger.error(error)
return
}
}

Closing


I hope that everyone has enjoyed this look at the inner workings of Swagger/openAPI and how to build a small API introspection tool into your own services/applications. If nothing else it hopefully gives you some sense of what goes into exposing and documenting services for tools like the SAP Business API Hub and the upcoming SAP Graph offering. Perhaps now you can begin to apply some of these same concepts and benefits to even your own internal or small scale APIs.
6 Comments