Back to integration tutorials
This tutorial helps you understand the basics of how Ballerina can be used to do client calls and develop RESTful APIs.
Overview
In this tutorial, you will develop a service that allows a user to retrieve a list of doctors based on the doctor's specialization (category). The information about the doctors is retrieved from a separate microservice.
To implement this use case, you will develop a REST service with a single resource using Visual Studio Code with the Ballerina Swan Lake extension and then run the service. The resource will receive the user request, retrieve details from the backend service, and respond to the user request with the relevant doctor details.
Concepts covered
- REST API
- HTTP client
Develop the application
Step 1: Set up the workspace
Install Ballerina Swan Lake and the Ballerina Swan Lake extension on VS Code.
Step 2: Develop the service
Follow the instructions given in this section to develop the service.
-
Create a new Ballerina project using the
bal
command and open it in VS Code.$ bal new sending-a-message-to-a-service
-
Remove the generated content in the
main.bal
file and open the diagram view in VS Code. -
Generate a record type corresponding to the payload from the backend service by providing a sample of the expected JSON payload.
The payload from the backend service will be an array of JSON objects, where each JSON object will be similar to the following.
{ "name": "thomas collins", "hospital": "grand oak community hospital", "category": "surgery", "availability": "9.00 a.m - 11.00 a.m", "fee": 7000 }
The generated record type will be as follows.
type Doctor record { string name; string hospital; string category; string availability; decimal fee; };
Note: While it is possible to work with the JSON payload directly, using record types offers several advantages including enhanced type safety, data validation, and better tooling experience (e.g., completion).
Note: When the fields of the JSON objects are expected to be exactly those specified in the sample payload, the generated records can be updated to be closed records, which would indicate that no other fields are allowed or expected.
-
Define the HTTP service (REST API) that has the resource that accepts user requests, retrieves relevant details from the backend service, and responds to the request.
-
Open the Ballerina HTTP API Designer in VS Code
-
Use
/healthcare
as the service path (or the context) of the service, which is attached to the listener listening on port8290
. -
Define an HTTP resource that allows the
GET
operation on the resource path/doctors
and accepts thecategory
path parameter (corresponding to the specialization). UseDoctor[]
,http:NotFound
, andhttp:InternalServerError
as the response types.The generated service will be as follows.
service /healthcare on new http:Listener(8290) { resource function get doctors/[string category]() returns Doctor[]|http:NotFound|http:InternalServerError { } }
-
-
Define a configurable variable for the URL of the backend service and an
http:Client
object to send requests to the backend service.The generated code will be as follows.
configurable string healthcareBackend = "http://localhost:9090/healthcare"; final http:Client queryDoctorEP = check new (healthcareBackend);
-
Implement the logic to retrieve and respond with relevant details.
service /healthcare on new http:Listener(8290) { resource function get doctors/[string category]() returns Doctor[]|http:NotFound|http:InternalServerError { log:printInfo("Retrieving information", specialization = category); Doctor[]|http:ClientError resp = queryDoctorEP->/[category]; if resp is Doctor[] { return resp; } if resp is http:ClientRequestError { return <http:NotFound> {body: string `category not found: ${category}`}; } return <http:InternalServerError> {body: resp.message()}; } }
-
The
log:printInfo
statement logs information about the request. -
The call to the backend is done using a remote method call expression (using
->
), which distinguishes network calls from normal method calls. Client data binding is used to directly try and bind the JSON response on success to the expected array of records. -
Use the
is
check to decide the response based on the response to the backend call. If the backend call was successful and the response payload was an array ofDoctor
records (as expected), then directly return the array from the resource. -
If the backend call fails, send an
http:NotFound
response if the client call failed with a4xx
status code or send anhttp:InternalServerError
response for other failures.
-
You have successfully developed the required service.
Complete source
import ballerina/http; import ballerina/log; type Doctor record {| string name; string hospital; string category; string availability; decimal fee; |}; configurable string healthcareBackend = "http://localhost:9090/healthcare"; final http:Client queryDoctorEP = check new (healthcareBackend); service /healthcare on new http:Listener(8290) { resource function get doctors/[string category]() returns Doctor[]|http:NotFound|http:InternalServerError { log:printInfo("Retrieving information", specialization = category); Doctor[]|http:ClientError resp = queryDoctorEP->/[category]; if resp is Doctor[] { return resp; } log:printError("Retrieving doctor information failed", resp); if resp is http:ClientRequestError { return <http:NotFound> {body: string `category not found: ${category}`}; } return <http:InternalServerError> {body: resp.message()}; } }
Sequence diagram
The sequence diagram view for the implemented resource method is the following.
Step 3: Build and run the service
Note: Alternatively, you can run this service by navigating to the project root and using the
bal run
command.sending-a-message-to-a-service$ bal run Compiling source integration_tutorials/sending_a_message_to_a_service:0.1.0 Running executable
Step 4: Try out the use case
Let's test the use case by sending a request to the service.
Start the backend service
Download the JAR file for the backend service, and execute the following command to start the service:
$ bal run hospitalservice.jar
Send a request
Use the Try it feature to send a request to the service. Specify surgery
as the path parameter.
Verify the response
You will see the response message from the backend with a list of details of the available doctors.
[ { "name": "thomas collins", "hospital": "grand oak community hospital", "category": "surgery", "availability": "9.00 a.m - 11.00 a.m", "fee": 7000.0 }, { "name": "anne clement", "hospital": "clemency medical center", "category": "surgery", "availability": "8.00 a.m - 10.00 a.m", "fee": 12000.0 }, { "name": "seth mears", "hospital": "pine valley community hospital", "category": "surgery", "availability": "3.00 p.m - 5.00 p.m", "fee": 8000.0 } ]
Now, check the terminal in which you ran the Ballerina service. You should see a log similar to the following.
time = 2023-08-15T13:01:34.022+05:30 level = INFO module = integration_tutorials/sending_a_message_to_a_service message = "Retrieving information" specialization = "surgery"
Step 5: Write tests for the use case
Let's test the use case by writing a test case that sends a request to the service and validates the payload for a successful request. Testing is enabled by the Ballerina test framework.
-
Change
queryDoctorEP
initialization in the source code to use a separate function that can be mocked.Replace
final http:Client queryDoctorEP = check new (healthcareBackend);
with
final http:Client queryDoctorEP = check initializeHttpClient(); function initializeHttpClient() returns http:Client|error => new (healthcareBackend);
-
Define an
http:Client
object to send requests to the healthcare service.final http:Client cl = check new (string `http://localhost:8290/healthcare/doctors`);
-
Define a variable with mock payload from the backend service. This variable will be used to mock the payload from the backend and to verify the received payload
map<json>[] & readonly surgeons = [ { "name": "thomas collins", "hospital": "grand oak community hospital", "category": "surgery", "availability": "9.00 a.m - 11.00 a.m", "fee": 7000.0d }, { "name": "anne clement", "hospital": "clemency medical center", "category": "surgery", "availability": "8.00 a.m - 10.00 a.m", "fee": 12000.0d }, { "name": "seth mears", "hospital": "pine valley community hospital", "category": "surgery", "availability": "3.00 p.m - 5.00 p.m", "fee": 8000.0d } ];
-
Mock the backend service by mocking the
http:Client
object and theget
resource method. Then, mock theinitializeHttpClient
function, using the@test:Mock
annotation, to return the mock HTTP client.public client class MockHttpClient { resource function get [string... path](map<string|string[]>? headers = (), http:TargetType targetType = http:Response, *http:QueryParams params) returns http:Response|anydata|http:ClientError { match path[0] { "surgery" => { return surgeons; } } return <http:ClientRequestError> error ("unknown specialization", body = string `unknown specialization: ${path[0]}`, headers = {}, statusCode = http:STATUS_NOT_FOUND); } } @test:Mock { functionName: "initializeHttpClient" } function initializeHttpClientMock() returns http:Client|error => test:mock(http:Client, new MockHttpClient());
-
Use the
@test:Config
annotation to indicate that a function is a test function. Implement the test to send a request to the service and test for value equality between the retrieved payload and the expected payload using thetest:assertEquals
function.@test:Config function testSuccessfulRequest() returns error? { Doctor[] doctors = check cl->/surgery; test:assertEquals(doctors, surgeons); }
-
Run the tests.
Alternatively, you can run all the tests in a package by navigating to the project root and using the
bal test
command.sending-a-message-to-a-service$ bal test Compiling source integration_tutorials/sending_a_message_to_a_service:0.1.0 Running Tests sending_a_message_to_a_service time = 2023-08-17T09:01:23.758+05:30 level = INFO module = integration_tutorials/sending_a_message_to_a_service message = "Retrieving information" specialization = "surgery" 1 passing 0 failing 0 skipped
You have now developed and tested a simple Ballerina REST service, which receives a request, logs a message, sends a request to a backend service, and responds to the original request with the response from the backend service.
Complete implementation
Check out the complete implementation on GitHub.