Ballerina HTTP services with compile-time type safety
Ballerina moves HTTP routing and payload validation to compile time. Services attached to listeners with verb-encoded resources, typed records for automatic marshalling, and explicit error types reduce runtime routing and marshalling bugs in cloud-native APIs.
25 Mar 2026, 17:11 UTC

Shipping an integration API that routes correctly and returns the right shape under load is a recurring source of production bugs. String-based routers and manual marshalling make path collisions and payload mismatches appear only at runtime. Ballerina’s declarative service model moves those checks to compile time by tying listeners, paths, and typed records together.
The practical thesis is simple: define the HTTP surface as a service with resources whose names encode the verb, type the request and response records, and let the compiler enforce uniqueness and shape. Errors become explicit in the type system and concurrency safety can be enforced with isolated.
Services attach to listeners, resources encode the verb
In Ballerina 2.x an HTTP server is a service bound to an http:Listener with a path prefix. Resources are functions inside the service whose name encodes the HTTP verb and path template. The compiler checks that paths are unique within a service and that parameter types match the template.
This eliminates a class of runtime routing errors common with string maps. A duplicate path or a mismatched parameter type is a compile error, not a 404 in production.
Typed payloads replace manual marshalling
Request and response bodies can be typed records. The http module maps Ballerina types to JSON and XML and performs content negotiation automatically. Returning a record serializes without explicit marshalling code.
Because the payload type is part of the resource signature, changes to the record propagate to callers and to generated metadata. The compiler rejects assignments where the return type does not match the declared type.
Errors are explicit and concurrency is opt‑in safe
Errors are a distinct type. A resource can declare returns error? and use the check expression to propagate failures explicitly. check panic terminates with a logged error. This makes failure modes visible in integration chains rather than hidden in try/catch blocks.
Service functions run in a concurrent worker model. Marking a resource isolated guarantees no shared mutable state is accessed, which encourages safe parallelism for I/O‑bound handlers without explicit locks. Isolated checking is strict; not all standard library functions are isolated, which can limit composition of existing code.
Worked example: typed order lookup
Create a file main.bal in a project root. The example assumes Ballerina 2.x with import ballerina/http.
import ballerina/http;
type Order record {|
string id;
string item;
decimal amount;
|};
service /orders on new http:Listener(8080) {
resource function get [string id](http:Request req) returns Order|error {
// placeholder lookup
if id == "" {
return error("missing id");
}
return {id: id, item: "widget", amount: 9.99};
}
}
Run from the project directory with bal run .. This requires the Ballerina CLI installed and write permission for the working directory. Expected check: the service starts and binds to port 8080. Risk: port in use will cause a startup failure. Do not run as root unless required by the chosen port.
To verify type safety, change the return type to a different record or add a second resource with the same path. The compiler should reject the program before execution.
Trade‑offs and limitations
Module names and import paths changed between Ballerina 1.x and 2.x, so example code differs by major version. Verify imports against your SDK.
Automatic OpenAPI generation reflects types but requires explicit annotations for advanced schemas and custom headers. Isolated checking can surface errors when composing non‑isolated library calls, which may force refactoring.
Practical verification steps: create a minimal service file with a service on http:Listener and run bal run to observe service start and respond to local requests. Use the compiler to test isolated violations by adding mutable state to a service function and observing compile errors. Inspect generated OpenAPI by printing the service metadata at runtime to confirm path and type mapping.
Use typed services for public APIs and integration boundaries where shape contracts matter. Keep resources small, return explicit error types, and prefer isolated for handlers that do not need shared mutable state.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.