Unit 15: Implement API operation
In this course you will learn how to implement API operations in the IBM DevOps Solution Workbench using your local machine. You will also practice how to apply error handling to the API operations.
Outlineโ
For the last course of the implementation phase, we will conclude with coding within the API Namespace. As explained in Unit 8: Design API Path and Operation, API paths together with their operations expose functionality of your service to the outside world. API operations have one or more responses, each of them consisting of a status code and a response body. Additionally they can contain a request body and parameters from different sources, either path, query or header. All the things mentioned above have an influence on the generated boilerplate code and implementation stubs for the API operations, so we can start to implement their actual logic.
In general the implementation of an API operation may contain the following pieces:
- Execute commands or services from the domain namespace
- Execute services from the integration namespace
- Map the request body, defined as an API schema, to the input of commands or services
- Map the output of commands and services to API schemas
- Return a response - depending on success or error - with the respective status code and body as an API schema.
Prerequisitesโ
- You have successfully completed Unit 10: Set up Project for Implementation.
- You have successfully completed Unit 8: Design API Path and Operation.
If you rather want to use this course as a starting point of your training, you can use a different asset where all courses for designing and the courses for implementing the domain and integration namespace are already completed.
In this case - depending on the chosen implementation language - use either asset "Order_Java_Code_0.3" or "Order_TypeScript_Code_0.3" to create a new project and to continue your training.
You can look up how to create a new project from the Order assets in the Preparation section in the Course Introduction.
Exerciseโ
Estimated time: 20 minutes
Exercise goal: After completing this course you are able to apply all pieces of implementing an API operation including their error handling.
Supported languages: Java and TypeScript
In this exercise we will implement API operation CreateOrder of the /orders path that we designed in the Solution Designer.
This operation will be triggered when a POST request is made to the /orders path. It will create a new order based on the provided data in the request body and return a response with the created order as its body.
Step 1: Implement the API operationโ
The API operation to create a new order has the following parts:
- Map the input of the API operation to the input for the command
CreateOrder - Execute the command and extract the created entity
- If no order entity was created, return a response with status code 400 and empty body
- Map the order entity to the order schema
- If the new order is a CustomerOrder, send the confirmation email via the
PostMailintegration service - Return a response with status code 201 and the order schema as its body
- Java
- TypeScript
-
Open the file
/src/main/java/<package-name>/api/v1/OrdersApiV1Provider.java.
You will see that the file contains an auto-generated stub with a classOrdersApiV1Providerand an empty methodcreateOrder. The implementation of the command needs to be added inside the method block.<package-name>: The package name of the Java project (e.g.com.knowis.orderjfinal)@Service
public class OrdersApiV1Provider implements OrdersApiV1Delegate {
@Override
public ResponseEntity<Order> createOrder(OrderCreateInput orderCreateInput) {
//TODO Auto-generated method stub
return createOrderExampleResponse();
}
} -
To reuse the implemented command
CreateOrderas well as the integration servicePostMail, we have to create a private field for classOrderCommandand for the integration service class. Additionally we have to create a constructor with two parameters that are then assigned to the respective private fields.
Furthermore we will create private field that enables logging since it is not part of the pre-generated code in API operations.private static final Logger log = LoggerFactory.getLogger(OrdersApiV1Provider.class);
private final OrderCommand orderCommand;
private final PostMail postMail;
public OrdersApiV1Provider(OrderCommand orderCommand, PostMail postMail) {
this.orderCommand = orderCommand;
this.postMail = postMail;
}Please use the auto-import function of your IDE to import the missing classes (and repeat this for the following steps). If you are not very familiar with imports, go back to Course 11 - Step 3: Import missing Classes for detailed information on class imports. Furthermore, you are free to compare your current imports with the recommended list of imports at the end of this section. Therefore, click here.
โ๏ธinfoIf you use a project which is created from one of the assets that are ready to use for the implementation courses (they have the phrase "Code" in their name), the private fields for logging and the
OrderCommandclass already exist. Then you just have to add the missing code for thePostMailclass. -
After injecting the fields, we will extend the
createOrdermethod in the following steps.
First, we have to remove the auto-generated lines in the method stub (see highlighted lines) from thecreateOrdermethod.@Override
public ResponseEntity<Order> createOrder(OrderCreateInput orderCreateInput) {
//TODO Auto-generated method stub
return createOrderExampleResponse();
} -
Then we will map the operation's input to the input entity of command
CreateOrder.// Step 1: Map the input of the api operation to the input for the command CreateOrder
CreateOrderInput createOrderInputEntity = new CreateOrderInputEntity();
createOrderInputEntity.setReferenceId(orderCreateInput.getReferenceId());
createOrderInputEntity.setOrderType(OrderType.valueOf(orderCreateInput.getOrderType().name()));
// Map the order items in the schema to the entity
List<com.knowis.orderjfinal.sdk.domain.ord.entity.OrderItem> orderItemsEntity = orderCreateInput.getOrderItems()
.stream()
.map(MappingUtils::mapOrderItemSchemaToEntity)
.toList();
createOrderInputEntity.setOrderItems(orderItemsEntity);Replace the phrase
com.knowis.orderjfinalwith the package name of your project (and repeat this for the following steps).MappingUtils: is the class handling the mapping between entities and API schemas. We added this class as preparation for the implementation phase in Course 10: Set up Project for Implementation -
Next, we will execute command
CreateOrderand extract the returned order entity.// Step 2: Execute the domain command and extract the created entity
com.knowis.orderjfinal.sdk.domain.ord.entity.Order orderEntity
= this.orderCommand.createOrder(createOrderInputEntity); -
In case the extracted entity is
null, we have to return a response with status code 400 and empty body. This indicates users that something went wrong when executing the API operation.// Step 3: If no order entity was created, return a response with BAD_REQUEST status and empty body
if (orderEntity == null) {
log.error("Order could not be created!");
return ResponseEntity.badRequest().build();
}ResponseEntity.badRequest.build(): creates a response with status 400 and empty body -
Then we will map the order entity to the Order schema using the
MappingUtilsclass.// Step 4: Map the order entity to the Order schema
Order orderSchema = MappingUtils.mapOrderEntityToSchema(orderEntity); -
Now we will use the
PostMailintegration service to send confirmation emails, if the new order is a customer order.// Step 5: If the new order is a CustomerOrder, send the confirmation email via the PostMail integration service
// Check the order type from the input
if (orderCreateInput.getOrderType().equals(OrderCreateInput.OrderTypeEnum.CUSTOMER)) {
// Create the input for the integration service
PostMailInput postMailInput = new PostMailInputEntity();
postMailInput.setMessageFormat(MessageFormat.HTML);
postMailInput.setSender("roboflow@knowis.email");
postMailInput.setTo(orderSchema.getReferenceId());
// Adding a "cc" is not required for the order confirmation
postMailInput.setSubject("RoboFlow Order Confirmation Email");
postMailInput.setMessage(
TextFormatter.format(
"The Order with id {} and reference {} was confirmed and is ready to be processed!",
orderSchema.getId(),
orderSchema.getReferenceId()
)
);
// Call the integration service
this.postMail.execute(postMailInput);
} -
At last, we will return a response with status code 201 and the order schema as its body. This indicates users that the API operation was executed successfully.
// Step 6: Return the order schema as a ResponseEntity with status code 201
return ResponseEntity.status(HttpStatus.CREATED).body(orderSchema);
The provided list supports you to compare your current imports with the recommended list, especially if your IDE complains about errors in the code at this stage of the course.
List of Imports - File OrdersApiV1Provider.java
// Fundamental 1: Generated classes based on design
import com.knowis.orderjfinal.sdk.api.v1.api.OrdersApiV1Delegate;
import com.knowis.orderjfinal.sdk.api.v1.model.Order;
import com.knowis.orderjfinal.sdk.api.v1.model.OrderCreateInput;
import com.knowis.orderjfinal.sdk.api.v1.model.OrderItem;
import com.knowis.orderjfinal.sdk.domain.facade.Repository;
import com.knowis.orderjfinal.sdk.domain.ord.entity.CreateOrderInput;
import com.knowis.orderjfinal.sdk.domain.ord.entity.CreateOrderInputEntity;
import com.knowis.orderjfinal.sdk.domain.ord.type.OrderType;
import com.knowis.orderjfinal.sdk.integration.email.entity.PostMailInput;
import com.knowis.orderjfinal.sdk.integration.email.entity.PostMailInputEntity;
// Fundamental 2: Generated classes reflecting the capabilities of the Workbench
import k5.sdk.springboot.util.text.TextFormatter;
// Fundamental 3: Built-in Java classes
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
// Fundamental 4: Classes from other implementation files
import com.knowis.orderjfinal.api.v1.utils.MappingUtils;
import com.knowis.orderjfinal.domain.ord.command.OrderCommand;
import com.knowis.orderjfinal.integration.email.service.PostMail;
The package name of the provided code is com.knowis.orderjfinal. All occurrences of this phrase have to be replaced with the package name of your project.
-
Open the file
/src-impl/api/v1/operations/CreateOrderApi.ts.
You will see that the file contains an auto-generated stub with a classCreateOrderApiand two more or less empty methods. The only content inside the methods is commented out sample code for some often-used functionality aimed at serving as a template for what you might want to do.export class CreateOrderApi extends V1BaseControllers.CreateOrderApiBase {
constructor(requestContext: RequestContext) {
super(requestContext);
}
/**
* @param request Request.CreateOrderRequest
* createOrder logic
*/
createOrder = async (request: Request.CreateOrderRequest) => {
this.util.log.info('start createOrder execution');
// this.response;
};
/**
* @param request Request.CreateOrderRequest
* @param error TypeError
* createOrderErrorHandler logic
*/
createOrderErrorHandler = async (error: TypeError, request: Request.CreateOrderRequest) => {
// TODO: error handling should go here
this.util.log.info('start createOrderErrorHandler execution');
// this.response;
};
}createOrder(): When fully implemented, this method provides the actual logic of the API operation.createOrderErrorHandler(): When fully implemented, this method provides the operation's error handling. -
We want to reuse the
Mapperclass we manually provided with content in preparation for the implementation phase. Since it needs a request context as its constructor parameter, we have to create a private field for theRequestContextclass and store the request context of our API operation as a private field. Additionally we have to assign the constructor parameter to the privae field.private context: RequestContext;
constructor(requestContext: RequestContext) {
super(requestContext);
this.context = requestContext;
} -
After adding the field, we will extend the
createOrdermethod in the following steps. First, we will create a local variable for theMapperclass to make the mapping-utilities available in our API operation.const mapper = new Mapper(this.context);Please use the auto-import function of your IDE to import the missing classes (and repeat this for the following steps). If you are not very familiar with imports, go back to Course 11 - Step 3: Import missing Classes for detailed information on class imports. Furthermore, you are free to compare your current imports with the recommended list of imports at the end of this section. Therefore, click here.
-
Then we will map the operation's input to the input entity of command
CreateOrderusing the mapper variable.// Step 1: Map the input of the api operation to the input for the command CreateOrder
const createOrderInput = mapper.mapRequestInputToCreateOrderInput(request.body);request.body: provides type-safe access to the request body that is modelled for the current operation. -
Next, we will execute command
CreateOrderand extract the returned order entity.// Step 2: Execute the domain command and extract the created entity
const orderEntity = await this.repo.ord.Order.CreateOrder(createOrderInput);this.repo.ord.Order.CreateOrder(): executes the Factory CommandCreateOrderwhich was defined for root entityOrderinside the domain namespaceord. This is an asynchronous operation and therefore needs to be awaited -
In case the extracted entity is
null, we have to throw an error which gets handled in thecreateOrderErrorHandlermethod.// Step 3: If no order entity was created, throw an exception
if (!orderEntity) {
throw new Error("Order could not be created!");
} -
Then we will map the order entity to the Order schema using the mapper variable.
// Step 4: Map the order entity to the order schema
const orderSchema = mapper.mapOrderEntityToSchema(orderEntity); -
Now we will use the
PostMailintegration service to send confirmation emails, if the new order is a customer order.// Step 5: If the new order is a CustomerOrder, send the confirmation email via the PostMail integration service
if (request.body.orderType === OrderCreateInputOrderTypeEnum.Customer) {
// Create the input for the integration service
const postMailInput = this.factory.entity.email.PostMail_Input();
postMailInput.messageFormat = EmailMessageMessageFormatEnum.Html;
postMailInput.sender = "roboflow@knowis.email";
postMailInput.to = request.body.referenceId;
// Adding a "cc" is not required for the order confirmation
postMailInput.subject = "RoboFlow Order Confirmation Email";
postMailInput.message = "The Order with id " + orderSchema.id + " and reference " + orderSchema.referenceId + " was confirmed and is ready to be processed!"
// Call the integration service
this.services.email.PostMail(postMailInput);
}this.factory.entity.email: provides the creation of instances of each entity in integration namespaceemailthis.services: allows to call any service modelled within a domain or integration namespace (as mentioned in a previous course) -
As a last step for the
createOrdermethod, we will assign the response's properties, its status code with 201 and its body with the order schema. This indicates users that the API operation was executed successfully.// Step 6: Set the response with status code 201 and the order schema as its body
this.response.body = orderSchema;
this.response.statusCode = 201;this.response.body: provides type-safe access to the response body of the operation. Only schemas that are modelled as response body are allowed to be set here.this.response.statusCode: is restricted to the status codes that are modeled for this operation. -
Now, we are complete with method
createOrderand we will continue with the error handling of the API operation. Here, we will handle all exception that are thrown the same way. Therefore, we will extend methodcreateOrderErrorHandlerwith code that simply sets the status code of the response to 400. This indicates users that something went wrong while executing the API operation.
As the response body should be empty in this case, we can already complete the error handling there.this.util.log.error(error);
this.response.statusCode = 400;
The provided list supports you to compare your current imports with the recommended list, especially if your IDE complains about errors in the code at this stage of the course.
List of Imports - File CreateOrderApi.ts
// Fundamental 1: Generated classes based on design
import { OrderCreateInputOrderTypeEnum } from 'solution-framework/dist/sdk/v1/namespace/apis/v1/model/order-create-input';
// Fundamental 2: Generated classes reflecting the capabilities of the Workbench
import {
Context as RequestContext,
V1BaseControllers,
V1Request as Request,
} from 'solution-framework';
// Fundamental 4: Classes from other implementation files
import { Mapper } from '../../../util/Mapper';
Step 2: Push changes to Gitโ
To make the changes of your implementation visible to others you have to push them to the remote Git repository.
- Go to your terminal and run command
k5 compileto ensure that you do not have any issues in your code. - Run the command
k5 push -m "Added implementation code".
You have successfully implemented an API operation! You also pushed your changes to your remote Git repository!
Now you are able to provide your own API operations with the actual logic.
You understand how elements from the domain and integration namespace can be used and how errors are handled in API operations.
What's Next?โ
As this course concludes the implementation phase of your training, you are now ready to continue with the deployment of your service. In the next course you will learn how to build and deploy your implemented service.