BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Ballerina for Full-Stack Developers: a Guide to Creating Backend APIs

Ballerina for Full-Stack Developers: a Guide to Creating Backend APIs

This item in japanese

Bookmarks

Key Takeaways

  • Full-stack developers no longer select a popular “stack.” Instead, they look at the best possible technology to implement their requirements at each layer in the “full stack.”
  • Frontends consume one or more backend APIs that are typically tailor-made to match the frontend UX. While RESTful services are the de facto standard for backend APIs, GraphQL and WebSocket APIs are emerging.
  • Ballerina is an open-source cloud native programming language that has inbuilt support for networking primitives, including listeners, services, remote/resource methods, and clients.
  • We first look at Ballerina’s intuitive syntax for writing REST APIs. Then at authentication, authorization, OpenAPI tool, observability, SQL/NoSQL client libraries, and key language features.
  • At the end of this article, you will have a good understanding of why Ballerina is a prominent candidate for writing your next backend API.

Background

A web app consists of 3 layers in the simplest form. Namely, the client side (frontend), server side (backend), and the persistence layer (database). Full-stack development refers to the practice of developing all 3 layers of a web application.

  • Frontend development involves HTML, CSS, and vanilla JavaScript or one or more JavaScript frameworks/libraries (like JQuery or React.js). Available tools, frameworks, and libraries have grown exponentially over the past few years. Therefore, frontend development itself is a very broad subject now.
  • Backend development usually involves a server-side scripting language (like PHP or JSP) or a service consumed by the frontend. With the emergence of single-page applications (SPA), developers have moved away from traditional server-side scripting languages and embraced APIs (REST, GraphQL, WebSocket, etc.) as the backend.
  • Persistence layer includes one or more SQL or NoSQL databases. But with the emergence of “Big Data,” data storage is no longer limited to conventional data stores. 

Popular Stacks

 

The MERN Stack- Image Source

Out of all the technologies used under each layer mentioned above, there were some technologies more popular than the others. When such technologies that cover all 3 layers were combined, we called it “a stack.” We have seen several popular stacks over the years.

  • LAMP / LEMP — JavaScript for the frontend. Linux for hosting, Apache/Nginx as the web server, MySQL as the persistence layer, and PHP as the backend (or server side) language. Frameworks like Laravel and Symphony were popular under this stack.
  • MEAN — MongoDB for the data persistence layer, Express as the backend framework, AngularJS as the frontend JavaScript framework, and NodeJS as the server runtime.
  • MERN — Same as MEAN stack, except that ReactJS had been chosen over Angular.
  • Ruby on Rails — A web application framework written in Ruby.
  • Django — JavaScript for the frontend, Python Django framework for the backend, and a suitable SQL or NoSQL database layer.

However, the stacks are not that simple anymore. For a given layer, available alternative technologies have multiplied. Within a given layer, the number of different technologies we can combine to develop the end product has also multiplied. 

Modern Full-Stack Development

When it comes to full-stack development, SPA (single-page applications) and PWA (progressive web applications) are becoming the norm and concepts like SSR (server-side rendering) have emerged to address limitations in them. These web applications (frontend) are supposed to work with a backend API (REST, GraphQL, etc.) to provide the ultimate functionality to the end users. With that, concepts like BFF (Backend for frontend) have emerged to align the backend APIs with the frontend user experience (UX).

BFF — Backend for Frontend

An organization can have several microservices whose services are used by different parties like mobile applications, web applications, other services/APIs, and external parties. Modern web applications however need a tightly coupled API to work closely with the frontend UX. Therefore, a BFF acts as the interface between the frontend and the microservices.

 

A BFF invokes multiple downstream services to construct a view in the front end. Downstream APIs can be of different types (REST, GraphQL gRPC, etc.). Read Pattern: Backends For Frontends for an in-depth understanding of BFF architecture patterns.

With the above concepts in mind, let’s discuss a bit more about modern web development.

Backend Development in the context of Full Stack Development

Developing backend APIs can mean 2 things:

  1. Developing a BFF — Acts as an adapter between the frontend UX and the backend microservices.
  2. Developing microservices — Developing the individual microservices which will be consumed directly or indirectly (via BFF) by a frontend.

In the context of full-stack development, we will only consider the backend APIs which are directly being invoked by a frontend. Those APIs can be written as BFF APIs or as separate microservices.

Choosing the Stack

Nowadays, developers don’t just go for a stack just because it’s popular. They choose the best frontend technologies to match the UI/UX they wish to implement. Then they choose the backend technologies considering several factors including the time to market, maintainability, and developer experience. 

In this article, I’m going to introduce a new promising candidate for backend development, Ballerina. In the future, you can consider it while evaluating backend technologies for your full-stack journey.

What is Ballerina?

 

Ballerina Programming Language Logo

Ballerina is an open-source cloud native programming language developed to make consuming, combining, and creating network services easier. Ballerina Swan Lake, the next major version of Ballerina language was released this year with major improvements to all aspects including an improved type system, enhanced SQL-like language-integrated queries, enhanced/intuitive service declarations, and many more.

The main motive behind ballerina is to enable developers to focus on business logic while spending less time on integrating cloud native technologies. The intuitive way of defining and running services on the cloud using Ballerina’s inbuilt network primitives plays a key role in this revolution. Flexible type system, data-oriented language-integrated queries, enhanced and intuitive concurrency handling along with inbuilt observability and tracing support, make Ballerina one of the go-to languages for the cloud era.

When developing APIs which are being invoked directly by the frontend, we have several popular choices:

  • REST APIs
  • GraphQL APIs
  • WebSocket APIs

Once a suitable API type is chosen, next we have to consider the following factors:

  • Secure Communication
  • Authentication
  • Authorization
  • Monitoring, Observability, and Logging

In addition to the above factors, we have to consider the maintainability and developer experience when developing the APIs. Let’s have a look at how Ballerina provides support for developing the above-mentioned API types and what makes Ballerina a promising candidate for backend API development.

Ballerina and Network Interactions

One of the main objectives of the Ballerina programming language is to simplify writing network interactions. With that in mind, Ballerina has networking primitives inbuilt to the language. While all the other mainstream programming languages treat networking as just another I/O resource, Ballerina has first-class support for network interactions. To facilitate that, Ballerina has the following first-class components:

  • Listeners — Acts as the interface between the networking layer and the service layer. A listener represents the underlying transport like HTTP, gRPC, TCP, or WebSocket.

  • Services — Represents a service that exposes an organization’s capabilities to the end users. HTTP, GraphQL, gRPC, and WebSocket are a few examples of such services.

  • Resource Methods — Represents a unit of functionality within a service. For example, if we consider a simple CRUD service to manage inventory, add inventory is represented by a single resource method while delete inventory operation is represented by another.

  • Clients — Writing a service these days usually includes invoking one or more external or internal services. For example:
    1. Within one of your services, you may want to send an email. For that, you need an email client.
    2. The same service may have a requirement to invoke one or more internal gRPC services. For that, you need gRPC clients.
    Similarly, writing services require invoking external services. For that, Ballerina has a rich concept called clients, and an external call is represented there by a remote method. Invoking a remote method in the Ballerina runtime is asynchronous (non-blocking, while no need for explicit callbacks or listeners).

These language inbuilt networking primitives coupled with other language features like explicit error handling, inbuilt json/xml support, and being flexibly typed help developers write intuitive and maintainable network interactions faster. This enables developers and organizations, in turn, to focus more on innovation.

 

Ballerina lang’s features at a glance

Let’s explore how we can write intuitive and meaningful backend APIs with Ballerina using its support for REST and GraphQL APIs. Please follow the getting started guide to install and set up Ballerina.

Setting up Ballerina

Developing REST APIs

Let’s have a look at how to write REST APIs with Ballerina.

Say “Hello World!”

A hello world REST API written in Ballerina looks like below:

import ballerina/http;
 
service / on new http:Listener(8080) {
 
   resource function get greeting() returns string {
       return "Hello!";
   }
}

Let’s decode each part of the syntax here:

  • import ballerina/http; — Imports the ballerina/http package
  • service /on new http:Listener(8080) — Creates a service with the context path "/"on an HTTP listener listening on port 8080. The type of the service is determined by the type of the listener attached. In this case, since our listener is an HTTP listener, this becomes an HTTP service.
  • resource function get greeting() returns stringRepresents a single operation that can be performed over this HTTP service. “get” is the “resource accessor.” In simple terms, it represents the HTTP Method (get, post, delete, etc.). “greeting”is the name of the function and the function name becomes the path. It means the resource path “/greeting” is handled by this resource method. “returns string” means that this service returns a string. We can return complex objects here also.
  • return "Hello World!"; — Represents the return value of the resource method. It’s the string “Hello World!”.

The following diagram shows an overview of the Ballerina HTTP service syntax:

 

Ballerina HTTP Service Structure (Source)

To get an in-depth understanding of the Ballerina HTTP service syntax, especially how to use query and path parameters, payload data binding, etc., please refer to the following article:

HTTP Deep-Dive with Ballerina: Services

An Integration Example — A Currency Conversion API

The following is a bit more complex REST API. Given a baseCurrency, targetCurrency, and an amount, this API returns the converted amount. To get the latest exchange rates, this API uses an external service.

import ballerina/log;
import ballerina/http;
 
configurable int port = 8080;
 
type ConversionResponse record {
   boolean success;
   string base;
   map<decimal> rates;
};
 
service / on new http:Listener(port) {
 
   resource function get convert/[string baseCurrency]/to/[string targetCurrency](decimal amount) returns decimal|error {
       http:Client exchangeEP = check new ("https://api.exchangerate.host");
       ConversionResponse response = check exchangeEP->get(string `/latest?base=${baseCurrency}`);
 
       if !response.success {
           return error("Exchange rates couldn't be obtained");
       }
 
       decimal? rate = response.rates[targetCurrency];
       if rate is () {
           return error("Couldn't determine exchange rate for target currency", targetCurrency = targetCurrency);
       }
 
       log:printInfo("converting currency", baseCurrency = baseCurrency, targetCurrency = targetCurrency, amount = amount);
 
       return rate * amount;
   }
}

Compared to the hello world example, this one demonstrates a few more interesting features of Ballerina.

  • service / on new http:Listener(port) — Base path is / now. And the port is configurable meaning it can be configured at the runtime. As defined in configurable int port = 8080, the port has a default value of 8080 and is configurable. Configurable variables is a noteworthy feature too.
  • resource function get convert/[string baseCurrency]/to/[string targetCurrency](decimal amount) returns decimal|error —In this case, the resource path is /convert/{baseCurrency}/to/{targetCurrency} and a query param named amount is required now. This resource method returns either a decimal (converted rate) or an error (which is mapped to 500 — Internal Server Error).
  • ConversionResponse response = check dccClient->get(string `/latest?base=${baseCurrency}`) — Invoke the external exchange rate API and converts the response to the open record ConversionResponse. This call is non-blocking. The response payload is seamlessly converted to an open record demonstrating Ballerina’s Open by Default principle.

Once run, a curl request like below will convert 100 USD to GBP.

curl http://localhost:8080/convert/USD/to/GBP?amount=100

Bonus: Low-Code Development

Ballerina has a leak-free graphical representation. That is, you can edit the source code and the low-code view (graphical representation) simultaneously. The following diagram is the low-code view of the above API:

Low-code view of the above currency conversion API.

Even though we will not be exploring the low-code aspect of Ballerina much, it is ideal for non-technical or less-technical people to understand and write code. Give it a try as well.

Leak Free — anything can be programmed in code and everything in code is visual.

A Simple CRUD Service

The following is an example CRUD service written in Ballerina. It manipulates a set of products that are kept in memory.

import ballerina/http;
import ballerina/log;
import ballerina/uuid;
 
# Represents a product
public type Product record {|
   # Product ID
   string id?;
   # Name of the product
   string name;
   # Product description
   string description;
   # Product price
   Price price;
|};
 
# An enum to represent currencies
public enum Currency {
   USD,
   LKR,
   SGD,
   GBP
}
 
# Represents price
public type Price record {|
   # Currency
   Currency currency;
   # Amount
   decimal amount;
|};
 
# Represents an error
public type Error record {|
   # Error code
   string code;
   # Error message
   string message;
|};
 
# Error response
public type ErrorResponse record {|
   # Error
   Error 'error;
|};
 
# Bad request response
public type ValidationError record {|
   *http:BadRequest;
   # Error response.
   ErrorResponse body;
|};
 
# Represents headers of created response
public type LocationHeader record {|
   # Location header. A link to the created product.
   string location;
|};
 
# Product Created response
public type ProductCreated record {|
   *http:Created;
   # Location header representing a link to the created product.
   LocationHeader headers;
|};
 
# Product updated response
public type ProductUpdated record {|
   *http:Ok;
|};
 
# The product service
service / on new http:Listener(8080) {
 
   private map<Product> products = {};
 
   # List all products
   # + return - List of products
   resource function get products() returns Product[] {
       return self.products.toArray();
   }
 
   # Add a new product
   #
   # + product - Product to be added
   # + return - product created response or validation error
   resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError {
       if product.name.length() == 0 || product.description.length() == 0 {
           log:printWarn("Product name or description is not present", product = product);
           return <ValidationError>{
               body: {
                   'error: {
                       code: "INVALID_NAME",
                       message: "Product name and description are required"
                   }
               }
           };
       }
 
       if product.price.amount < 0d {
           log:printWarn("Product price cannot be negative", product = product);
           return <ValidationError>{
               body: {
                   'error: {
                       code: "INVALID_PRICE",
                       message: "Product price cannot be negative"
                   }
               }
           };
       }
 
       log:printDebug("Adding new product", product = product);
       product.id = uuid:createType1AsString();
       self.products[<string>product.id] = product;
       log:printInfo("Added new product", product = product);
 
       string productUrl = string `/products/${<string>product.id}`;
       return <ProductCreated>{
           headers: {
               location: productUrl
           }
       };
   }
 
   # Update a product
   #
   # + product - Updated product
   # + return - A product updated response or an error if product is invalid
   resource function put product(@http:Payload Product product) returns ProductUpdated|ValidationError {
       if product.id is () || !self.products.hasKey(<string>product.id) {
           log:printWarn("Invalid product provided for update", product = product);
           return <ValidationError>{
               body: {
                   'error: {
                       code: "INVALID_PRODUCT",
                       message: "Invalid product"
                   }
               }
           };
       }
 
       log:printInfo("Updating product", product = product);
       self.products[<string>product.id] = product;
       return <ProductUpdated>{};
   }
 
   # Deletes a product
   #
   # + id - Product ID
   # + return - Deleted product or a validation error
   resource function delete products/[string id]() returns Product|ValidationError {
       if !self.products.hasKey(<string>id) {
           log:printWarn("Invalid product ID to be deleted", id = id);
           return {
               body: {
                   'error: {
                       code: "INVALID_ID",
                       message: "Invalud product id"
                   }
               }
           };
       }
 
       log:printDebug("Deleting product", id = id);
       Product removed = self.products.remove(id);
       log:printDebug("Deleted product", product = removed);
       return removed;
   }
}

While most of the syntax is self-explanatory, this service has 4 resource methods:

  • List all products — GET /products
  • Add a new product — POST /products
  • Update a product — PUT /product
  • Delete a product — DELETE /products/{id}

Note how the types have been defined to represent products, price, and currency. Then we have defined response types where necessary to achieve the desired schema. ProductCreated is to represent the response of adding a product. ValidationError is to represent an error in validation.

# Bad request response
public type ValidationError record {|
   *http:BadRequest;
   # Error response.
   ErrorResponse body;
|};
 
# Product Created response
public type ProductCreated record {|
   *http:Created;
   # Location header representing a link to the created product.
   LocationHeader headers;
|};

Having such a schema helps developers understand the code easily. Just by looking at the resource method definition, developers can get a clear overview of the resource method. What’s the resource path, what query/path parameters are required, what’s the payload, and what are the possible return types.

resource function post products(@http:Payload Product product) returns ProductCreated|ValidationError {
 
}

It’s a POST request, sent to the /products (derived by looking at the resource method’s), requires a payload of type Product and returns either a validation error (400) or an HTTP CREATED response (201) with a location header.

Generating the OpenAPI Specification

Once we have the service written in Ballerina, you can simply generate the OpenAPI specification just by pointing to the source file. It will output the OpenAPI specification with corresponding status codes and schema by looking at the source.

You can read more on that under the OpenAPI section:

Ballerina OpenAPI Tool

Generating a complete OpenAPI spec helps you to generate required clients. In our case, to generate JavaScript clients and integrate our frontend with the backend easily.

Securing Services

You can secure your service by updating the HTTP listener to an HTTPS listener as follows.

http:ListenerSecureSocket secureSocket = {
   key: {
       certFile: "../resource/path/to/public.crt",
       keyFile: "../resource/path/to/private.key"
   }
};
service /hello on new http:Listener(8080, secureSocket = secureSocket) {
   resource function get world() returns string {
       return "Hello World!";
   }
}

You can enable mutual SSL and do advanced configurations as well. Refer to the Ballerina examples on HTTP service security for more information.

Authentication

Ballerina has inbuilt support for 3 authentication mechanisms.

JWT

You can either provide the cert file or the JWKs endpoint URL of your authorization server and enable JWT signature validation. For example, if we are to protect our service with an IDaaS (Identity as a Service) like Asgardeo, we just have to add the following annotation to the service:

@http:ServiceConfig {
   auth: [
       {
           jwtValidatorConfig: {
               signatureConfig: {
                   jwksConfig: {
                       url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks"
                   }
               }
           }
       }
   ]
}

Additionally,

  • You can protect the entire service or only a subset of resource paths.
  • You can validate the issuer or the audience in the JWT.
  • You can perform authorization based on claims (explained later).

Refer to the REST API Security section of Ballerina examples to learn more.

OAuth2

Similarly to JWT, you can protect your services with OAuth2. Refer the Service - OAuth2 example for more details.

Basic Auth

For basic auth, 2 user store options are available; file and LDAP. Please refer to the following examples to see how it is done:

Authorization

With OAuth2 and JWT you can validate scopes per service or per resource. In both cases, you can specify a custom scope key. The default is scope. 

In the case of JWT, you can use a custom claim containing user roles (role-based access control — RBAC) or permissions (fine-grained access control) to authorize individual operations.

import ballerina/http;
 
public type Product record {|
   string id?;
   string name;
   string description;
|};
 
Product[] products = [];
 
@http:ServiceConfig {
   auth: [
       {
           jwtValidatorConfig: {
               issuer: "wso2",
               audience: "example.com",
               scopeKey: "permissions",
               signatureConfig: {
                   jwksConfig: {
                       url: "https://api.asgardeo.io/t/imeshaorg/oauth2/jwks"
                   }
               }
           }
       }
   ]
}
service /products on new http:Listener(8080) {
 
   @http:ResourceConfig {
       auth: {
           scopes: "product:view"
       }
   }
   resource function get .() returns Product[] {
       return products;
   }
 
   @http:ResourceConfig {
       auth: {
           scopes: "product:create"
       }
   }
   resource function post .(@http:Payload Product product) returns error? {
       products.push(product);
   }
}

As shown above, the /products service validates if the incoming JWT has the product:view permission to list products and the product:create permission to create a product. The scopeKey is set to permissions which is the name of the claim in the JWT to look at for validation. Additionally, it validates the issuer and the audience.

Clients

Obviously, you have to communicate with external services when writing a backend API. At least you will need a database client. Be it a DB client, HTTP client or gRPC client, Ballerina got you covered very well. Most importantly, client calls are non-blocking in Ballerina with developers not having to add any callbacks or listeners.

Check out how convenient the clients are in Ballerina:

Additionally, Ballerina has rich support for resiliency. See following examples to get an idea on them:

GraphQL APIs

To keep this article short, I’m not going to go in-depth on writing GraphQL APIs. However, similarly to the REST APIs, Ballerina has the same level of support for GraphQL services. Please refer to the following links to read more on this:

Read more on GraphQL in the Reference by Example section of the Ballerina website for more information on writing GraphQL services.

WebSocket APIs

I will not be discussing this in-depth as well. Please refer to the WebSockets and WebSocket security sections under the Reference by Example section of the Ballerina website for more details on writing WebSocket services.

Observability

Observability is one of the key features inbuilt to the language. With Ballerina, you can perform distributed tracing and metric monitoring out of the box.

Tracing

Distributed tracing is available via Jaeger and Choreo. In order to publish traces to Jaeger (or Choreo), you just have to add an import to your code. During runtime, your services will then publish traces to Jaeger (or Choreo) using the Open Telemetry standards.

 

Distributed tracing with Jaeger and Ballerina

Logging

The logging framework provided by Ballerina is ideal for log analysis using logstash and similar log analyzers. While writing code, you can pass additional key value pairs to the log line.

log:printInfo("This is an example log line", key1 = "key1", payload = { id: 123});

The output of the above log line looks like:

time = 2022-01-26T16:19:38.662+05:30 level = INFO module = "" message = "This is an example log line" key1 = "key1" payload = {"id":123}

Metrics

Real-time metrics can be monitored with Prometheus and grafana. Additionally, you can monitor live metrics using Choreo.. 

 

Observability view in Choreo (source)

Similarly to distributed tracing, you just have to add an import to your source code and import a ready-made grafana dashboard to publish and monitor real-time metrics.

 

Real-time metrics Grafana dashboard for Ballerina

Read more on Ballerina observability features from the following links:

Persistence Layer

The next major aspect in backend development is the persistence layer. Ballerina has a rich list of SQL and NoSQL DB clients.

DB calls made via clients are non-blocking in Ballerina

SQL

The following clients are available as of now:

Additionally, Ballerina comes with a very convenient way of writing prepared statements using RawTemplates.

mysqlClient->execute(`insert into products (product_name, price, currency) values (${product.productName}, ${product.price}, ${product.currency})`);
 
mysqlClient->execute(`update products set product_name = ${product.productName}, price = ${product.price}, currency=${product.currency} where id=${product?.id}`);

In the above example, ${<variableName>} represents variables bound to the query and the above piece of code is executed as a prepared statement in the runtime.

Data Binding and Streams

Similarly, we can fetch data as a stream of a user-defined type using select queries as follows. Suppose we have a Product record as below:

type Product record {|
   int id?;
   string productName;
   float price;
   string currency;
|};

And the product table is defined as below:

CREATE TABLE `products` (
 `id` int NOT NULL AUTO_INCREMENT,
 `product_name` varchar(255) NOT NULL,
 `price` float DEFAULT NULL,
 `currency` varchar(5) DEFAULT NULL,
 PRIMARY KEY (`id`)
)

You can fetch data as a stream of Product records as below:

stream<Product, error?> productStream = mysqlClient->query(`select id, product_name as productName, price, currency from products`);

Note that the record field names and fetched column names are similar.

NoSQL

On top of the usual benefits of NoSQL, Ballerina’s inbuilt JSON and open record types come in handy when working with unstructured or semi-structured data.

Note: The Ballerina ecosystem is still growing. Therefore, a fully capable ORM library is not yet available.

Other Noteworthy Features

Inbuilt JSON/XML Support

As we have already seen, Ballerina has inbuilt support for popular wire formats, JSON, and XML. With Ballerina, you can seamlessly convert between user-defined types and JSON as we saw in HTTP services and DB examples.

Structural Type System

Instead of nominal typing (as in Java/Kotlin), Ballerina relies on the shape of the construct (like in Go/TypeScript) to determine subtypes. This allows developers to seamlessly convert among user-defined types and among user-defined types and JSON.

Statically Typed

Ballerina is statically typed. This enables Ballerina to provide a rich set of tools to write reliable and maintainable code. At the same time, Ballerina follows the “open by default” principle where you only have to define what you are interested in. Open records are one example of this usage.

Explicit Error Handling

Errors should be handled explicitly. As we saw in examples, client calls return a union of the result and an error. Developers should type check for errors and handle them explicitly.

Null Safe

Ballerina is null safe

Maintainable

With all the above aspects combined, Ballerina becomes a maintainable and reliable language tailored for network programming.

Graphical

As we have seen briefly, Ballerina has a really good, leak-free low-code aspect. Ballerina uses sequence diagrams to visualize network interactions. This is really good for less technical and non-technical people to program and to understand a program.

Summary

In this article, I wanted to give a brief, but comprehensive overview of the Ballerina programming language’s support for writing backend APIs. We covered how we can write REST APIs in depth. And we looked at how to secure services, how to perform authentication and authorization, and generating OpenAPI specifications.

Next, we looked briefly at how we can write GraphQL and WebSocket services. Then we looked at the observability features and the persistence layer support (for SQL and NoSQL databases). Finally, we looked at a few of the Ballerina language features which are noteworthy.

I think the discussed content will be helpful for you to explore the Ballerina programming language more. And to understand its prominence in the context of backend API development. I hope I was able to establish the fact that Ballerina is a true cloud native programming language that treats network primitives as first-class citizens. I invite you to explore more on the Ballerina programming language.

Thank you for following this article. Feel free to share your opinions and suggestions. Also, reach out to me or the Ballerina community if you have further questions.

About the Author

Rate this Article

Adoption
Style

BT