Code-First REST APIs Followup: OpenAPI
Fri Aug 26 11:04:08 EDT 2022
- Code-First REST APIs With XPages Jakarta EE Support
- Code-First REST APIs Followup: OpenAPI
- XAgents to Jakarta REST Services
In yesterday's post, I gave a two-file example of writing a basic CRUD REST API for NSF documents. In that post, I casually mentioned that one of the side benefits of this approach would have to wait until I fixed an open bug.
Well, I fixed that bug not long after making that post, so now I can detail what that is.
But just before I do that, I should mention that I added an "examples" directory to the project repository, where I plan to put examples like this in on-disk-project form, without the baggage of the test-suite example NSFs in the main tree: https://github.com/OpenNTF/org.openntf.xsp.jakartaee/tree/develop/examples. Anyway, back to what I fixed up here.
One of the neat little side features that the framework brings in is MicroProfile OpenAPI, which automatically generates OpenAPI specifications for your REST services based on your code. Depending on your workflow, this can be tremendously convenient. OpenAPI, being a widely-supported spec, has tons of tools available, and you can use this when integrating other applications with yours, or when working in a multi-tiered development team. For example, if the UI portion of your app is being developed separately from the back end, you could hand off the OpenAPI file to the other developer(s) and they will have the information they need to write against your services. And, since it's generated from the code and not manually, it has the benefit of being inherently consistent with the current design of the app.
Default Output
By default, based on how the app worked when we left it yesterday, going to "foo.nsf/xsp/app/openapi.yaml" will get you this output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | --- openapi: 3.0.3 info: title: Jakarta Code-First REST servers: - url: http://some.server/foo.nsf/xsp/app paths: /employees: get: responses: "200": description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Employee' post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Employee' responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Employee' /employees/{id}: get: parameters: - name: id in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Employee' put: parameters: - name: id in: path required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/Employee' responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Employee' delete: parameters: - name: id in: path required: true schema: type: string responses: "204": description: No Content components: schemas: Employee: required: - name - title - department type: object properties: id: type: string name: minLength: 1 type: string nullable: false title: minLength: 1 type: string nullable: false department: minLength: 1 type: string nullable: false age: format: int32 minimum: 1 type: integer |
This includes all of the operations we defined in the rest.EmployeesResource
class as well as the definition of the model.Employee
entity class. Additionally, it picked up on our Bean Validation annotations, and so all of the @NotEmpty
properties are marked as being non-null and non-empty strings, while the age has a minimum of 1
, as coded.
Expanding the Definition
That, on its own, is pretty useful, and it will automatically adapt to any code changes you make. However, you can go further.
Versions
For example, while having the file as it is will work for development, you'll want to give it a version when it goes into production, so that any API consumers can know when it's expected that the API changed. There are two ways to do this with this project. If you have a $TemplateBuild
shared field with a template version, then the code will pick up on that. Alternatively, you can specify configuration properties via MicroProfile Config. To do that, create a new file in the "Code/Java" directory of the project in the Package Explorer view in Designer named "microprofile-config.properties" within a directory named "META-INF":
If you don't have a Package Explorer pane, you can add it by going to Window and then either switching to the "XPages" perspective or going to "Show Eclipse Views" and picking it from there. To add the folder and then the file, you can right-click the "Code/Java" folder there and then going to "New" - "Other..." and picking each in turn.
Once you have that file open, add a line like this:
1 | mp.openapi.extensions.smallrye.info.version=1.0.1 |
("SmallRye" is the name of several MicroProfile spec implementations)
Then save. Now, when you open the OpenAPI spec, it'll start like this:
1 2 3 4 5 | ---
openapi: 3.0.3
info:
title: Jakarta Code-First REST
version: 1.0.1
|
Now, as long as you update this for API changes or use a $TemplateBuild
field, your OpenAPI will be nicely versioned. As a nice bonus, if you build your NSF using the NSF ODP Tooling project, it can add the Maven version to $TemplateBuild
by default, so you don't have to worry about manual updates.
Endpoint Descriptions
Next, while all of our endpoints are listed, it'd be good to add some additional detail. While they're more-or-less clear now, it'll get less so as the app grows. This is generally done via annotations - there are a bunch of them, but we'll focus on just a few for now.
We'll start with a basic one: adding a description to the endpoint that lists all Employees. To do this, go back to rest.EmployeeResource
and add an annotation of type org.eclipse.microprofile.openapi.annotations.Operation
:
1 2 3 4 5 6 | @GET @Produces(MediaType.APPLICATION_JSON) @Operation(description="Retrieves a list of all employee entities in the data store") public List<Employee> get() { return employees.findAll(Sorts.sorts().asc("name")).collect(Collectors.toList()); } |
Once you add that, then that part of the OpenAPI spec will read:
1 2 3 4 5 6 7 8 9 10 11 12 13 | paths: /employees: get: description: Retrieves a list of all employee entities in the data store responses: "200": description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Employee' |
Better by one step. The @Operation
annotation itself has a couple more properties, which let you specify an operationId
(very useful for code generated from the spec, so I advise doing it in a fully-fledged app) or marking the operation as "hidden", so it won't show up in the output at all.
Model Annotations
Next up, we'll add some descriptive information to the Employee model itself. For this, we'll go back to model.Employee
and start adding annotations of type org.eclipse.microprofile.openapi.annotations.media.Schema
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Schema(description = "Represents an individual employee within the system") @Entity public class Employee { /* snip */ private @Id String id; @Schema(description="The employee's full name", example="Foo Fooson") private @Column @NotEmpty String name; @Schema(description="The employee's job title", example="CTO") private @Column @NotEmpty String title; @Schema(description="The name of the employee's current department within the company", example="IT") private @Column @NotEmpty String department; @Schema(description="The employee's current age", example="80") private @Column @Min(1) int age; /* snip */ } |
The @Schema
annotation is usable in a lot of situations and has a lot of options, but these will suffice for now. Once we add these, our OpenAPI spec expands in the components
section to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | components: schemas: Employee: description: Represents an individual employee within the system required: - name - title - department type: object properties: id: type: string name: description: The employee's full name minLength: 1 type: string example: Foo Fooson nullable: false title: description: The employee's job title minLength: 1 type: string example: CTO nullable: false department: description: The name of the employee's current department within the company minLength: 1 type: string example: IT nullable: false age: format: int32 description: The employee's current age minimum: 1 type: integer example: 80 |
Now, anyone reading this (or interpreting it with a tool) will have just a bit more information about it. In this case, the descriptions don't add much, but you can imagine expanding this to cover your business specific business rules for formatting, internal codes, etc..
Swagger
Though there's a lot more that you can do to expand the OpenAPI generation, I'll leave it there for now. I'll finish up here with one of the more straightforward benefits you get from this: using Swagger UI. Swagger UI is a tremendously-popular tool for visualizing (and, to an extent, working with) OpenAPI specifications. You can download Swagger UI yourself (to run locally or put in your NSF) or use the live demo, which runs in your browser.
If you want to use the live demo, you can enable CORS in the microprofile-config.properties file created earlier, setting rest.cors.enable
to true
and rest.cors.allowedOrigins
to *
.
Once you have it accessible, you can point Swagger UI to your URL, like "http://some.server/foo.nsf/xsp/app/openapi.yaml", and it'll generate a nice summary:
You can imagine either handing that off to your front-end developer or using it yourself when working on the client part of your system. As you expand your spec - say, adding @Tag
to categorize your resources - the UI will expand to reflect it as well.
Conclusion
I'm quite fond of the MicroProfile OpenAPI spec here - it's easy to use and you don't have to worry about the fiddly work of actually generating the spec. Additionally, it's an excellent example of the kind of benefits you get from building on top of Jakarta and MP specifications: because they're built by the active involvement and many companies and with an eye towards interoperability, you automatically get to use tools like Swagger UI or OpenAPI Generator that have no knowledge of Domino. You're rowing in the same direction as lots of others.
In the short term, I plan to update the examples section of the project Git repo with the newer version of these classes, and then follow up by putting the "GitHub issues" client code I wrote for my recent OpenNTF presentation in as another example. Once I do the latter, I'll make sure to post about it as well.