In this article, we will talk about the feature of the CUBA platform that exists for quite a long time, but is still not widely known - front-end SDK generator, and see how it works with CUBA’s REST API addon.
Java+JavaScript - marriage born in web
Only eight years ago we, Java developers, used JavaScript as a "second-class citizen" language in our web applications. Back in those days its purpose was adding some dynamics to the web pages generated on the server-side with frameworks like JSF, Struts, Tapestry or Thymeleaf. Nowadays we are witnessing an emerge of JavaScript as the language number one for the client-side development with such frameworks as React, Vue or Angular, and with Node.js it even comes to the server-side.
In reality, we develop web applications that may use different languages on different tiers: JavaScript for the client-side UI, Java for the business logic processing, SQL to fetch data from the database, Python for data analysis, etc. And we have to combine all those languages into one application using various technologies. The most common example - REST API. Based on platform-agnostic HTTP protocol and simple JSON format, it is now the default method to stitch the client-side JS and the server-side Java.
But even the best stitch cannot be seamless. There is always a problem with API definition: which methods to call, what the data model is and whether we should pass a street address as a structured object or just as a string.
How can we help our JavaScript fellows to create their code faster and avoid miscommunication?
Is Swagger the ultimate answer?
"Swagger" you say and right you are. Swagger is de-facto an industrial standard for designing, building, documenting and consuming REST API. There is a number of code generators that help with client SDK generation for different languages.
CUBA Framework supports Swagger, every application with REST API add-on has the endpoint that allows downloading Swagger documentation in .json or .yaml format. You can use these files to generate a JS client.
Please consider the fact that Swagger is only an API documentation tool. But what kind of info a front-end developer wants to see in the API? The “classic” approach is: map business functions to services and build a well-defined API. Then expose it as a set of REST services, add Swagger documentation and enjoy.
Then why does GraphQL hit the trends making a buzz among front-end developers? And note that GraphQL share in web APIs world is growing. What is going on? It turned out that sometimes it is easier to give front-end developers a bit more “generic” API to avoid creating a lot of small APIs for use cases that might change frequently. E.g. for your shopping basket in the web UI, you need an order with prices only first, then an order with totals, etc. GraphQL is also a great tool to avoid both overfetching and underfetching as well as querying several APIs at once to get a complex data structure.
OK, it looks like the application should expose not only services but also some generic APIs. This approach allows front-end developers to invoke complex business functions and gives them some degree of flexibility, so they won’t request API changes if they need just a different data representation for the UI.
And there is one more problem that neither Swagger nor GraphQL or OData solves - what to do with the generated client code if something is changed. Direct one-time code generation is simple, but support is a different thing. How can we be sure that our front-end application will not fail after we remove an entity’s property?
So, to speed up front-end development and simplify collaboration between back-end team and front-end team we need to:
- Expose both business-specific and generic API
- Generate front-end code based on the back-end data model and method signatures
- Modify generated code with minimum efforts and potential bugs
We face all these challenges in CUBA with REST API add-on and front-end SDK generator.
CUBA TypeScript SDK
In CUBA, REST API add-on provides the following functionality:
- CRUD operations over the data model
- Execution of predefined JPQL queries
- Execution of services methods
- Getting metadata (entities, views, enumerations, datatypes)
- Getting current user permissions (access to entities, attributes, specific permissions)
- Getting current user information (name, language, time zone, etc.)
- Working with files
So we have everything we need to work with the application from any front-end client. All those APIs are described in swagger YAML or JSON file, so you can start implementing the application right away.
It is very important to set up security rules for the REST API users to prevent accidental endpoint exposure to all users. First, deny general REST API access for all users and then create special permissions for roles that need to access the desired functionality.
But CUBA offers more than just REST API. You can generate an SDK that can be used as a basis for any front-end development framework: React, Angular, Vue or other.
With the generator, you create a set of TypeScript classes that allows you to invoke CUBA API from your client application.
To generate the SDK, you just need to run
npm install -g @cuba-platform/front-generator
And then
gen-cuba-front sdk:all
and all classes will be created for you. You can even generate a simple UI based on ReactJS, so your customers will be able to start working instantly with the CUBA-based application. The UI is pretty basic, but with CUBA you will get all the features instantly, including authentication, role-based access to data, entity graph retrieval, etc.
Let’s have a closer look at what the SDK has.
Data Model
The application data model is represented as the set of TypeScript classes. If we have a look a the Session Planner application used in the QuickStart, there is an entity there:
@NamePattern("%s %s|firstName,lastName")
@Table(name = "SESSIONPLANNER_SPEAKER")
@Entity(name = "sessionplanner_Speaker")
public class Speaker extends StandardEntity {
@NotNull
@Column(name = "FIRST_NAME", nullable = false)
protected String firstName;
@Column(name = "LAST_NAME")
protected String lastName;
@Email
@NotNull
@Column(name = "EMAIL", nullable = false, unique = true)
protected String email;
//Setters and getters here
}
And in the SDK we’ll get a class:
export class Speaker extends StandardEntity {
static NAME = "sessionplanner_Speaker";
firstName?: string | null;
lastName?: string | null;
email?: string | null;
}
All associations and compositions will be preserved, therefore you will be able to fetch an entity graph instead of fetching entities one-by-one using several API calls.
No more DTOs - you’ll get exactly the same data as described on the back-end.
Business Services
All the services exposed via REST in CUBA will have a TypeScript representation in the SDK. For example, if we expose the Session Service using REST API, you’ll get a TypeScript code that looks like this:
export var restServices = {
sessionplanner_SessionService: {
rescheduleSession: (cubaApp: CubaApp, fetchOpts?: FetchOptions) => (params: sessionplanner_SessionService_rescheduleSession_params) => {
return cubaApp.invokeService("sessionplanner_SessionService", "rescheduleSession", params, fetchOpts);
}
}
};
Therefore you will be able to call it from the UI just by writing the following line:
restServices.sessionplanner_SessionService.rescheduleSession(cubaREST)({session, newStartDate}).then( (result) => {
//Result handling
});
Handy, isn’t it? All the routine work is done for you.
Generic API
If you need to implement custom logic for front-end only, you can always use a set of functions defined in the core CUBA platform REST library such as:
loadEntities<T>(entityName: string, options?: EntitiesLoadOptions, fetchOptions?: FetchOptions): Promise<Array<SerializedEntity<T>>>;
deleteEntity(entityName: string, id: any, fetchOptions?: FetchOptions): Promise<void>;
Those functions give you a fine-grained access to CRUD operations with entities in the application. Security still stays, CUBA verifies all non-anonymous calls on the server-side and prevents fetching entities or attributes that do not comply with a user’s role.
cubaREST.loadEntities<Speaker>(Speaker.NAME).then( (result => {
//Result handling
}));
Using this generic API, a developer can create a JS application with the custom API layer created over the generic CRUD and deploy it to the node.js server implementing “backend for frontend” architectural pattern. Moreover, there may be more than one API layer implemented with this approach, we can implement a different set of APIs for different clients: ReactJS, Native iOS, etc. In fact, the generated SDK is the ideal tool for this use case.
What is not great with the generic API is the risk of underfetching or overfetching data when you either fetch more attributes than you need to or you don’t have enough attributes in the API descriptors. CUBA’s Entity Views solve this problem on the back-end and we provide the same option for front-end developers! For each generated TypeScript class we create types that reflect views:
export type SpeakerViewName = "_minimal" | "_local" | "_base";
export type SpeakerView<V extends SpeakerViewName> =
V extends "_minimal" ? Pick<Speaker, "id" | "firstName" | "lastName"> :
V extends "_local" ? Pick<Speaker, "id" | "firstName" | "lastName" | "email"> :
V extends "_base" ? Pick<Speaker, "id" | "firstName" | "lastName" | "email"> :
never;