Echo Service in Ballerina: Complete Solution & Deep Dive Guide

A ballerina poses gracefully in a dance.

Mastering Ballerina Services: A Zero-to-Hero Guide to REST APIs

Learn to build a powerful Ballerina echo service from scratch. This guide covers creating REST API endpoints, handling GET requests, processing query parameters with @http:QueryParam, and defining resource paths to create a fully functional, modern web service using Ballerina's intuitive, network-aware syntax.

Have you ever felt bogged down by the sheer amount of boilerplate code required just to get a simple web server running? In many popular frameworks, setting up a basic API endpoint can feel like a ceremony of configurations, dependencies, and abstract classes. You just want to handle a request and return some data, but you find yourself lost in a maze of controllers, services, and repositories before you can write a single line of business logic.

This complexity is a common pain point for developers, leading to slower development cycles and a steeper learning curve. Ballerina, a modern, open-source programming language, was designed from the ground up to solve this exact problem. It treats network services not as an afterthought, but as a core feature of the language itself.

In this comprehensive guide, we'll demystify the process of building REST APIs. We will build a simple yet powerful "Echo Service" from the ground up, demonstrating how Ballerina's elegant syntax and powerful features make network programming intuitive and efficient. By the end, you won't just have a working API; you'll have a solid foundation for building complex, production-ready microservices.


What Exactly is a Ballerina Service?

In the world of Ballerina, a service is not just a design pattern or a class with a special annotation; it's a first-class citizen of the language. It's a fundamental building block designed specifically to represent a network-accessible service, like a REST API, a GraphQL endpoint, or a gRPC service. Think of it as a container for a collection of network-callable functions.

A Ballerina service is attached to a listener, which is an object that listens for incoming network requests on a specific port and protocol. The most common listener is the http:Listener, which, as the name suggests, listens for HTTP requests. This direct, explicit connection between a listener and a service is a core concept that makes Ballerina code exceptionally clear and self-documenting.

Inside a service, you define resource functions. Each resource function maps directly to an API endpoint, combining an HTTP method (like GET, POST, PUT) and a resource path. This structure eliminates the need for complex routing configurations found in other frameworks. The network endpoint is defined right where the logic lives, making the code easier to read, maintain, and reason about.


// A basic Ballerina service structure
import ballerina/http;

service / on new http:Listener(9090) {

    // A resource function mapping to: GET /hello
    resource function get hello() returns string {
        return "Hello, World!";
    }

}

This clean, declarative syntax is a cornerstone of Ballerina's design philosophy, aiming to make distributed systems integration seamless and robust.


Why Choose Ballerina for Your Next Web Service?

When starting a new project, the choice of technology stack is critical. While established players like Java/Spring, Python/Django, and Node.js/Express dominate the landscape, Ballerina presents a compelling, modern alternative specifically engineered for the age of microservices and cloud-native applications. Its unique features offer significant advantages in network programming.

However, like any technology, it comes with its own set of trade-offs. Understanding both the strengths and weaknesses is key to making an informed decision. Here’s a balanced look at why you might—or might not—choose Ballerina for your next API.

Advantages and Disadvantages of Ballerina for APIs

Feature / Aspect Pros (Advantages) Cons (Disadvantages)
Language Design Designed specifically for network integration. Services, listeners, and clients are first-class citizens, reducing boilerplate and improving clarity. As a newer language, the developer talent pool is smaller compared to giants like Java, Python, or JavaScript.
Concurrency Built-in, lightweight concurrency with strands (similar to Goroutines) makes handling thousands of concurrent requests efficient and straightforward without complex threading models. The concurrency model, while powerful, requires a shift in thinking for developers accustomed to traditional single-threaded or manual multi-threaded environments.
Type Safety Ballerina is statically typed, catching many potential data-related errors at compile time rather than runtime. Its type system is flexible and designed to handle network data like JSON seamlessly. Static typing can sometimes feel more verbose for very small scripts or prototypes compared to dynamically typed languages like Python or JavaScript.
Tooling & Visualization The VS Code plugin automatically generates sequence diagrams, providing a visual representation of the network interactions in your code. This is incredibly useful for debugging and documentation. The ecosystem of third-party libraries and tools is still growing and is not as vast as those for more established languages. You might have to write your own connector for a niche service.
Data Handling Excellent, built-in support for JSON and XML. Data binding is often automatic and intuitive, simplifying the process of parsing request bodies and generating responses. While core data formats are well-supported, handling more obscure or legacy data formats might require more manual effort.
Future-Proofing Ballerina's focus on cloud-native principles, microservices, and API-first design positions it well for future software development trends. Being a younger project, its long-term trajectory and adoption rates are still being established. Major breaking changes between versions are possible as the language matures.

How to Build the Ballerina Echo Service: A Step-by-Step Guide

Now, let's get our hands dirty and build the Echo Service. This practical exercise from the kodikra learning path is a perfect introduction to the core concepts of Ballerina services. We'll create two distinct endpoints to handle different types of requests.

Prerequisites

Before we begin, ensure you have the Ballerina compiler and toolchain installed. You can verify your installation by running the following command in your terminal:


$ bal version
Ballerina 2201.8.4 (Swan Lake Update 8)
Language specification 2023R1
Update tool 1.3.14

A code editor with Ballerina support, like Visual Studio Code with the official Ballerina extension, is also highly recommended for a smooth development experience.

Step 1: Create a New Ballerina Project

The Ballerina CLI makes it easy to scaffold a new project. Navigate to your desired directory and run the bal new command.


$ bal new echo_service
Created new Ballerina project at 'echo_service'

This command creates a new directory named echo_service with a default main.bal file and a Ballerina.toml configuration file. We will be working inside the main.bal file.

Step 2: The Complete Solution Code

Let's replace the contents of main.bal with our complete service definition. This code creates an HTTP service with two resource functions (endpoints).


import ballerina/http;

// Define a service that attaches to a listener on port 9090.
// The base path for all resources within this service is "/".
service / on new http:Listener(9090) {

    // This is the first resource function, our echo endpoint.
    // It maps to the HTTP request: GET /echo
    // The '@http:QueryParam' annotation is key here. It tells Ballerina
    // to look for a query parameter named "sound" in the URL and
    // automatically bind its value to the 'sound' function parameter.
    // Example URL: http://localhost:9090/echo?sound=HelloBallerina
    resource function get echo(@http:QueryParam string sound) returns string|error {
        // The logic is simple: return the value of the 'sound' parameter.
        // If the 'sound' parameter is missing, Ballerina will automatically
        // respond with a 400 Bad Request error, which is a great built-in feature.
        return sound;
    }

    // This is the second resource function, our definition endpoint.
    // It maps to a hierarchical path: GET /definition/echo
    // Ballerina handles nested paths intuitively.
    // This function takes no parameters and returns a static string.
    resource function get definition/echo() returns string {
        // Return a fixed definition string as the response body.
        return "A sound or series of sounds caused by the reflection of sound waves from a surface back to the listener.";
    }

}

Step 3: A Detailed Code Walkthrough

Let's break down the code piece by piece to understand exactly what's happening.

  1. import ballerina/http;
    This line imports Ballerina's standard HTTP library. This module provides all the necessary types and functions for building HTTP clients, services, and listeners, including http:Listener, @http:QueryParam, and more.

  2. service / on new http:Listener(9090) { ... }
    This is the service declaration.
    • service / on: This declares a new service. The / specifies the base path. All resource paths inside this service will be relative to this root path.
    • new http:Listener(9090): This creates a new instance of an HTTP listener and binds it to port 9090. This listener is responsible for accepting incoming TCP connections and decoding the HTTP requests.

  3. resource function get echo(...)
    This defines our first API endpoint.
    • resource function: This special type of function is used within a service to define an accessible resource.
    • get: This keyword specifies the HTTP method this function will respond to. It can be get, post, put, delete, etc.
    • echo: This is the resource path, relative to the service's base path. So, the full path for this endpoint is /echo.

  4. @http:QueryParam string sound
    This is the magic of Ballerina's data binding.
    • @http:QueryParam: This is an annotation that instructs the HTTP listener to extract a query parameter from the request URL.
    • string sound: It looks for a parameter named sound and expects its value to be a string. This value is then automatically assigned to the local function parameter named sound. If the parameter is missing or of the wrong type, Ballerina automatically sends a client error (400 Bad Request) without you needing to write any validation code.

  5. returns string|error
    This defines the function's return type. It can return either a string (on success) or an error. Ballerina's HTTP module will automatically map a returned string to an HTTP response with a 200 OK status and the string as the body.

  6. resource function get definition/echo() ...
    This defines our second endpoint. The key difference is the path definition/echo. Ballerina interprets this as a hierarchical path, making it accessible via GET /definition/echo. It takes no parameters and simply returns a static string.

ASCII Art: Request Flow for the `/echo` Endpoint

This diagram illustrates how an incoming request is processed by our Ballerina service to generate a response.

  ● Client sends GET request
  │   "http://localhost:9090/echo?sound=Hey"
  │
  ▼
┌───────────────────┐
│ Network Interface │
└─────────┬─────────┘
          │
          ▼
┌───────────────────────────┐
│ Ballerina http:Listener   │
│       (Port 9090)         │
└─────────────┬─────────────┘
              │
              ▼
    ◆ Route Request to Service (`/`)
              │
              ▼
    ◆ Match Resource: `get echo`
              │
              ▼
┌─────────────────────────────────┐
│ Execute `echo` function         │
│ ├─ Annotation `@http:QueryParam`│
│ └─ Extracts "sound" → "Hey"     │
└─────────────────┬───────────────┘
                  │
                  ▼
    ◆ Logic: `return sound;`
              │
              ▼
┌───────────────────────────┐
│ Construct HTTP Response   │
│ ├─ Status: 200 OK         │
│ └─ Body: "Hey"            │
└─────────────┬─────────────┘
              │
              ▼
  ● Client receives response

Step 4: Running and Testing the Service

With the code in place, running the service is a single command. In your terminal, from within the echo_service project directory, execute:


$ bal run
Compiling source
        kodikra/echo_service:0.1.0

Running executable

[ballerina/http] started HTTP/WS listener 0.0.0.0:9090

Your service is now live and listening for requests on port 9090. Let's test it using curl, a command-line tool for making web requests.

Test 1: The `/echo` Endpoint

Open a new terminal window and run the following command. We are passing "BallerinaIsAwesome" as the value for the sound query parameter.


$ curl "http://localhost:9090/echo?sound=BallerinaIsAwesome"

You should see the exact same string echoed back to you as the response:


BallerinaIsAwesome

Test 2: The `/definition/echo` Endpoint

Now, let's test the second endpoint. This one doesn't require any parameters.


$ curl http://localhost:9090/definition/echo

The server should respond with the static definition we provided in the code:


A sound or series of sounds caused by the reflection of sound waves from a surface back to the listener.

To stop the service, simply go back to the terminal where it's running and press Ctrl + C.


Advanced Concepts & Alternative Approaches

The basic echo service is a great starting point, but real-world APIs often require more flexibility. Let's explore some common variations and advanced patterns you can use to enhance your Ballerina services.

Handling Optional Query Parameters

What if you want a query parameter to be optional? For example, if the sound parameter is not provided, you might want to use a default value instead of having Ballerina return an error. You can achieve this by making the parameter type nilable using a question mark (?).


// ... inside the service
resource function get echo(@http:QueryParam string? sound) returns string {
    // Check if the 'sound' parameter was provided.
    // If 'sound' is '()', it means the parameter was absent in the URL.
    if sound is () {
        return "Silence...";
    } else {
        return "You said: " + sound;
    }
}

Now, if you call curl http://localhost:9090/echo without the parameter, it will gracefully respond with "Silence...".

Using Default Values for Query Parameters

An even more concise way to handle optional parameters is to provide a default value directly in the function signature. Ballerina will use this value if the parameter is omitted from the request.


// ... inside the service
// If 'sound' is not provided, it will default to "default sound".
resource function get echo(@http:QueryParam string sound = "default sound") returns string {
    return "The sound is: " + sound;
}

This approach is cleaner when you just need a simple fallback and don't require complex logic for the missing parameter case.

Path Parameters vs. Query Parameters

So far, we've only used query parameters (the part after the ?). Another common way to pass data is through path parameters, which are part of the URL path itself. The choice between them is a key aspect of good API design.

  • Path Parameters (@http:PathParam) are used to identify a specific resource. They are mandatory parts of the path. Example: /users/123, where 123 is a path parameter identifying a specific user.
  • Query Parameters (@http:QueryParam) are used to sort, filter, or paginate a collection of resources. They are typically optional. Example: /users?sort=name&page=2.

Here's how you'd define a resource with a path parameter in Ballerina:


// This resource function maps to paths like /users/42, /users/101, etc.
resource function get users/[string id]() returns string {
    return "Fetching data for user with ID: " + id;
}

ASCII Art: Choosing Between Path and Query Parameters

This decision flow diagram can help you decide which type of parameter to use when designing your API endpoints.

    ● Start: Designing a new endpoint
    │
    ▼
┌──────────────────────────────────┐
│ What is the purpose of the data? │
└─────────────────┬────────────────┘
                  │
                  ▼
◆ Is it essential to identify a unique resource?
  (e.g., user ID, product SKU)
     ╱                       ╲
   Yes ───────────────► ┌────────────────────┐
   │                    │ Use Path Parameter │
   │                    │ e.g., /orders/{id} │
   │                    └────────────────────┘
   No
   │
   ▼
◆ Is it for filtering, sorting, or paginating a list?
  (e.g., status=active, sort=desc, limit=10)
     ╱                       ╲
   Yes ───────────────► ┌─────────────────────┐
   │                    │ Use Query Parameter │
   │                    │ e.g., /items?color=blue │
   │                    └─────────────────────┘
   No
   │
   ▼
◆ Is it optional configuration for the endpoint's behavior?
     ╱                       ╲
   Yes ───────────────► ┌─────────────────────┐
   │                    │ Use Query Parameter │
   │                    │ e.g., /report?format=pdf │
   │                    └─────────────────────┘
   No
   │
   ▼
    ● Re-evaluate endpoint design

Frequently Asked Questions (FAQ)

1. What's the difference between a `service` and a `function` in Ballerina?
A standard function is a reusable block of code that you call from other parts of your program. A service, on the other hand, is a network-accessible construct that exposes its logic to the outside world via a listener. The functions inside a service, called resource functions, are not called directly by your code but are invoked by the listener in response to incoming network requests.

2. How do I handle errors in a Ballerina resource function?
You can return specific HTTP error types from the ballerina/http module. For instance, instead of letting Ballerina handle a missing parameter, you could check for a condition and return a custom error: return http:NotFound("The requested resource does not exist."); or return http:InternalServerError("An unexpected error occurred.");. This gives you fine-grained control over status codes and response bodies.

3. Can I use other HTTP methods like POST or PUT?
Absolutely. You simply change the accessor keyword in the resource function signature. For example, resource function post echo(...) would respond to POST requests, and resource function put users/[string id](...) would respond to PUT requests. Ballerina provides accessors for all standard HTTP methods.

4. How do I change the port my Ballerina service runs on?
You change the integer value passed to the http:Listener constructor. For example, to run the service on port 8080, you would change the service declaration to: service / on new http:Listener(8080).

5. Is it possible to return JSON instead of a plain string?
Yes, and it's one of Ballerina's greatest strengths. You can define a record type (similar to a struct or class) and return an instance of it. Ballerina's HTTP module will automatically serialize the record into a JSON object and set the Content-Type header to application/json. For example: resource function get user() returns UserRecord { return { id: 1, name: "Alice" }; }.

6. What does the `/` in `service / on ...` mean?
This is the service's base path. It acts as a prefix for all resource paths defined within that service. If you set it to service /api/v1 on ..., then the echo resource function would be accessible at the full path /api/v1/echo. This is extremely useful for versioning your APIs.

7. How does Ballerina handle concurrent requests to the same service?
Ballerina has a highly efficient concurrency model based on "strands," which are lightweight threads managed by the runtime. Each incoming request is processed on its own strand, allowing a single Ballerina service to handle thousands of concurrent connections simultaneously without the complexity of manual thread management. This is a core feature that makes Ballerina ideal for high-performance network applications.

Conclusion: Your Journey into Network Programming

Congratulations! You have successfully built, tested, and understood a fully functional REST API using Ballerina. We've moved from the basic concept of a service to the practical implementation of endpoints, exploring how Ballerina's first-class support for network primitives like listeners, resource functions, and data-binding annotations dramatically simplifies web service development.

The Echo Service, though simple, encapsulates the core principles that make Ballerina a powerful choice for modern software development. You've seen how its clear, concise syntax reduces boilerplate and makes code more readable and maintainable. The built-in features for handling query parameters, defining resource paths, and managing concurrency empower you to focus on your application's logic rather than wrestling with complex framework configurations.

This is just the beginning. The skills you've acquired here are the foundation for building sophisticated microservices, integrating with third-party APIs, and orchestrating complex distributed systems. As you continue your journey, you'll discover more of Ballerina's rich feature set, designed to make you a more productive and effective developer in a connected world.

To continue learning, we encourage you to explore our complete Ballerina language guide or dive into the next module on the kodikra.com learning roadmap.

Disclaimer: The code and concepts in this article are based on Ballerina Swan Lake Update 8 (2201.8.x) and later versions. Syntax and features may differ in older or future versions of the language.


Published by Kodikra — Your trusted Ballerina learning resource.