Back to integration tutorials
This tutorial helps you understand the basics of Ballerina constructs, which allow you to do client calls, develop RESTful APIs, and send emails using the SMTP protocol.
Overview
In this tutorial, you will develop a service that accepts requests to make an appointment at a hospital, makes the appointment, and sends an email to the client confirming the appointment details.
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. This resource will receive the user request, reserve the appointment, and send an email to the user with the appointment details.
The flow is as follows.
-
Receive a request with a JSON payload in the following form.
{ "patient": { "name": "John Doe", "dob": "1940-03-19", "ssn": "234-23-525", "address": "California", "phone": "8770586755", "email": "johndoe@gmail.com", "cardNo": "7844481124110331" }, "doctor": "thomas collins", "hospital_id": "grandoaks", "hospital": "grand oak community hospital", "appointment_date": "2023-10-02" }
-
Extract necessary details from the request (e.g., hospital, patient, doctor, etc.) and make a call to the hospital backend service to request an appointment. A response similar to the following will be returned from the hospital backend service on success.
{ "appointmentNumber": 1, "doctor": { "name": "thomas collins", "hospital": "grand oak community hospital", "category": "surgery", "availability": "9.00 a.m - 11.00 a.m", "fee": 7000.0 }, "patient": { "name": "John Doe", "dob": "1940-03-19", "ssn": "234-23-525", "address": "California", "phone": "8770586755", "email": "johndoe@gmail.com" }, "hospital": "grand oak community hospital", "confirmed": false, "appointmentDate": "2023-10-02" }
-
Call the payment backend service to make the payment to confirm the appointment. If the payment is successful, the response payload will be similar to that shown below.
{ "appointmentNo": 1, "doctorName": "thomas collins", "patient": "John Doe", "actualFee": 7000, "discount": 20, "discounted": 5600.0, "paymentID": "8458c75a-c8e0-4d49-8da4-5e56043b1a20", "status": "settled" }
-
If the payment is successful, send an email to the user with the appointment details.
Concepts covered
- REST API
- HTTP Client
- Email Client
Develop the application
Step 1: Set up the workspace
Install Ballerina Swan Lake and the Ballerina Swan Lake VS Code 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-emails-from-a-service
-
Remove the generated content in the
main.bal
file and open the diagram view in VS Code. -
Generate record types corresponding to the payloads from the hospital and payment backend services by providing samples of the expected JSON payloads.
The payload from the hospital backend service will be a JSON object similar to the following.
{ "appointmentNumber": 1, "doctor": { "name": "thomas collins", "hospital": "grand oak community hospital", "category": "surgery", "availability": "9.00 a.m - 11.00 a.m", "fee": 7000.0 }, "patient": { "name": "John Doe", "dob": "1940-03-19", "ssn": "234-23-525", "address": "California", "phone": "8770586755", "email": "johndoe@gmail.com" }, "hospital": "grand oak community hospital", "confirmed": false, "appointmentDate": "2023-10-02" }
The generated records will be as follows.
type Patient record { string name; string dob; string ssn; string address; string phone; string email; }; type Doctor record { string name; string hospital; string category; string availability; decimal fee; }; type Appointment record { int appointmentNumber; Doctor doctor; Patient patient; boolean confirmed; string hospital; string appointmentDate; }; type Payment record { int appointmentNo; string doctorName; string patient; decimal actualFee; int discount; decimal discounted; string paymentID; string status; }; type PatientWithCardNo record { *Patient; string cardNo; }; type ReservationRequest record { PatientWithCardNo patient; string doctor; string hospital_id; string hospital; string appointment_date; }; type ReservationResponse record { int appointmentNo; string doctorName; string patient; decimal actualFee; int discount; decimal discounted; string paymentID; string status; };
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 sends an email to the client.
-
Open the Ballerina HTTP API Designer in VS Code.
-
Use
/healthcare
as the service path (or the context) for the service attached to the listener that is listening on port8290
. -
Define an HTTP resource that allows the
POST
operation on the resource path/categories/{category}/reserve
and accepts thecategory
path parameter (corresponding to the specialization). UseReservationRequest
as a parameter indicating that the resource expects a JSON object corresponding toReservationRequest
as the payload. Usehttp:Created
,http:NotFound
, andhttp:InternalServerError
as the response types.The generated service will be as follows.
service /healthcare on new http:Listener(8290) { resource function post categories/[string category]/reserve(ReservationRequest payload) returns http:Created|http:NotFound|http:InternalServerError { } }
-
-
Define configurable variables for the URLs of the backend services. Also, define configurable variables for the host, username, and password of the SMTP client.
The generated code will be as follows.
configurable string hospitalServicesBackend = "http://localhost:9090"; configurable string paymentBackend = "http://localhost:9090/healthcare/payments"; configurable string host = "smtp.gmail.com"; configurable string username = ?; configurable string password = ?;
Note: Enable two factor authentication on your Google account, generate an app password, and use the app password in place of your email password. The app password can be generated with this link.
-
Define two
http:Client
objects to send requests to the backend services and oneemail:SmtpClient
object to send emails.The generated code will be as follows.
final http:Client hospitalServicesEP = check new (hospitalServicesBackend); final http:Client paymentEP = check new (paymentBackend); final email:SmtpClient smtpClient = check new (host, username, password);
-
Implement the logic.
service /healthcare on new http:Listener(8290) { resource function post categories/[string category]/reserve(ReservationRequest payload) returns http:Created|http:NotFound|http:InternalServerError { PatientWithCardNo patient = payload.patient; Appointment|http:ClientError appointment = hospitalServicesEP->/[payload.hospital_id]/categories/[category]/reserve.post({ patient: { name: patient.name, dob: patient.dob, ssn: patient.ssn, address: patient.address, phone: patient.phone, email: patient.email }, doctor: payload.doctor, hospital: payload.hospital, appointment_date: payload.appointment_date }); if appointment !is Appointment { log:printError("Appointment reservation failed", appointment); if appointment is http:ClientRequestError { return <http:NotFound>{body: string `unknown hospital, doctor, or category`}; } return <http:InternalServerError>{body: appointment.message()}; } int appointmentNumber = appointment.appointmentNumber; Payment|http:ClientError payment = paymentEP->/.post({ appointmentNumber, doctor: appointment.doctor, patient: appointment.patient, fee: appointment.doctor.fee, confirmed: appointment.confirmed, card_number: patient.cardNo }); if payment !is Payment { log:printError("Payment settlement failed", payment); if payment is http:ClientRequestError { return <http:NotFound>{body: string `payment failed: unknown appointment number`}; } return <http:InternalServerError>{body: payment.message()}; } email:Error? sendMessage = smtpClient->sendMessage({ to: patient.email, subject: "Appointment reservation confirmed at " + payload.hospital, body: getEmailContent(appointmentNumber, appointment, payment) }); if sendMessage is email:Error { return <http:InternalServerError>{body: sendMessage.message()}; } return <http:Created>{}; } } function getEmailContent(int appointmentNumber, Appointment appointment, Payment payment) returns string => let Patient patient = appointment.patient, Doctor doctor = appointment.doctor in string `Appointment Confirmation Appointment Details Appointment Number: ${appointmentNumber} Appointment Date: ${appointment.appointmentDate} Patient Details Name: ${patient.name} Contact Number: ${patient.phone} Doctor Details Name: ${doctor.name} Specialization: ${doctor.category} Payment Details Doctor Fee: ${payment.actualFee} Discount: ${payment.discount} Total Fee: ${payment.discounted} Payment Status: ${payment.status}`;
-
The first backend call is a
POST
request to the hospital service to reserve the appointment. Thehospital_id
andcategory
values are used as path parameters. -
Use the
is
check to decide the flow based on the response to the client call. If the request failed with a4xx
status code, return anhttp:NotFound
response. Else, if the payload could not be bound toAppointment
as expected or if there were any other failures, respond with anhttp:InternalServerError
response. -
If the appointment reservation was successful, we can make the payment by making a
POST
request to the payment service. The payload includes details extracted out from the original request (forcard_number
) and the appointment reservation response (forappointmentNumber
,doctor
,patient
,fee
, andconfirmed
). -
If the payment was successful, the next and final step is to send an email to the user with the appointment details. We send the email specifying the user's email address, the subject, and the email body.
-
If the email is sent successfully, the response will be an
http:Created
response. If the email sending process resulted in an error, anhttp:InternalServerError
response will be returned.
-
You have successfully developed the required service.
Complete source
import ballerina/email; import ballerina/http; import ballerina/log; type Patient record {| string name; string dob; string ssn; string address; string phone; string email; |}; type Doctor record {| string name; string hospital; string category; string availability; decimal fee; |}; type Appointment record {| int appointmentNumber; Doctor doctor; Patient patient; boolean confirmed; string hospital; string appointmentDate; |}; type Payment record {| int appointmentNo; string doctorName; string patient; decimal actualFee; int discount; decimal discounted; string paymentID; string status; |}; type PatientWithCardNo record {| *Patient; string cardNo; |}; type ReservationRequest record {| PatientWithCardNo patient; string doctor; string hospital_id; string hospital; string appointment_date; |}; type ReservationResponse record {| int appointmentNo; string doctorName; string patient; decimal actualFee; int discount; decimal discounted; string paymentID; string status; |}; configurable string hospitalServicesBackend = "http://localhost:9090"; configurable string paymentBackend = "http://localhost:9090/healthcare/payments"; configurable string host = "smtp.gmail.com"; configurable string username = ?; configurable string password = ?; final http:Client hospitalServicesEP = check new (hospitalServicesBackend); final http:Client paymentEP = check new (paymentBackend); final email:SmtpClient smtpClient = check new (host, username, password); service /healthcare on new http:Listener(8290) { resource function post categories/[string category]/reserve(ReservationRequest payload) returns http:Created|http:NotFound|http:InternalServerError { PatientWithCardNo patient = payload.patient; Appointment|http:ClientError appointment = hospitalServicesEP->/[payload.hospital_id]/categories/[category]/reserve.post({ patient: { name: patient.name, dob: patient.dob, ssn: patient.ssn, address: patient.address, phone: patient.phone, email: patient.email }, doctor: payload.doctor, hospital: payload.hospital, appointment_date: payload.appointment_date }); if appointment !is Appointment { log:printError("Appointment reservation failed", appointment); if appointment is http:ClientRequestError { return <http:NotFound>{body: string `unknown hospital, doctor, or category`}; } return <http:InternalServerError>{body: appointment.message()}; } int appointmentNumber = appointment.appointmentNumber; Payment|http:ClientError payment = paymentEP->/.post({ appointmentNumber, doctor: appointment.doctor, patient: appointment.patient, fee: appointment.doctor.fee, confirmed: appointment.confirmed, card_number: patient.cardNo }); if payment !is Payment { log:printError("Payment settlement failed", payment); if payment is http:ClientRequestError { return <http:NotFound>{body: string `payment failed: unknown appointment number`}; } return <http:InternalServerError>{body: payment.message()}; } email:Error? sendMessage = smtpClient->sendMessage({ to: patient.email, subject: "Appointment reservation confirmed at " + payload.hospital, body: getEmailContent(appointmentNumber, appointment, payment) }); if sendMessage is email:Error { return <http:InternalServerError>{body: sendMessage.message()}; } return <http:Created>{}; } } function getEmailContent(int appointmentNumber, Appointment appointment, Payment payment) returns string => let Patient patient = appointment.patient, Doctor doctor = appointment.doctor in string `Appointment Confirmation Appointment Details Appointment Number: ${appointmentNumber} Appointment Date: ${appointment.appointmentDate} Patient Details Name: ${patient.name} Contact Number: ${patient.phone} Doctor Details Name: ${doctor.name} Specialization: ${doctor.category} Payment Details Doctor Fee: ${payment.actualFee} Discount: ${payment.discount} Total Fee: ${payment.discounted} Payment Status: ${payment.status}`;
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-emails-from-a-service$ bal run Compiling source integration_tutorials/sending-emails-from-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. Use the following as the request payload.
{ "patient": { "name": "John Doe", "dob": "1940-03-19", "ssn": "234-23-525", "address": "California", "phone": "8770586755", "email": "johndoe@gmail.com", "cardNo": "7844481124110331" }, "doctor": "thomas collins", "hospital_id": "grandoaks", "hospital": "grand oak community hospital", "appointment_date": "2023-10-02" }
Verify the email
You will receive an email with information similar to the following for a successful appointment reservation.
Complete implementation
Check out the complete implementation on GitHub.