Spending Less Time on Boilerplate
How I Build Projects
The Options
Actually Using Blackbird API Development Platform
Getting Blackbird API Platform
Generating My Project
Moving Forward
Conclusion
How Blackbird let me focus on what was important
In today’s market, time is an extremely valuable commodity. I’d like to share my thoughts on the current ways to help make the most efficient use of developers’ time and effort. I’m a developer at heart and even though I am helping to build it, here’s what gets me excited about using Blackbird API Development Platform to develop Blackbird.
How I Build Projects
Starting off, a little about my process. I tend to start a project by building out Open API specification that will define the contract between the frontend, backend, and any other external clients. It’s important to have a solid understanding of these contracts to ensure my team and I can move fast in parallel and OpenAPI provides a well adopted standard for doing so. Once the spec is done, work can proceed in parallel. Now me, I’ve always been a backend person, so I’m going to focus on that part.
The Options
Today, AI is all the rage, so I started looking at GenAI and the various tools out there that can help out. But what I found, is that the current state of GenAI makes it really good at giving snippets of solutions, but going for a whole project is fraught with challenges. As things stand now, the answers are just not deterministic enough for me to accept without a high degree of changes needing to be made. So that one was off my list.
Next up was looking at more traditional Open API specification. There are OpenAPI code generation libraries out there that are pretty good for client code, so I decided to check out the server side capabilities. And was soon disappointed. While client side codegen is well supported, there were many aspects of the server side offerings that I was unhappy with from lack of support to lack of flexibility.
In comes Blackbird. Blackbird API Development is a new tool from Ambassador that aims to improve the development process and efficiency. One of the features of Blackbird is code generation from an Open API specification. The difference here is that in addition to basic conversion of the Open API specification into code, Blackbird allows you to use templates to customize the output from the code generation. There is a default template that sets up a good project, but you can write your own templates. This is the sort of flexibility that excites me in a tool.
Actually Using Blackbird API Development Platform
Let’s start out with looking at how it was used with building out our user management service. To help me get started, I followed the quickstart to get an account setup. From there, we’ll start off looking at the Open API specification that I’ll be using.
{"openapi": "3.0.1","info": {"title": "User Service","version": "1.0","description": "API for managing users"},"tags": [{"name": "User","description": "Operations related to users"}],"paths": {"/register": {"post": {"summary": "Register a new user","operationId": "registerUser","tags": ["User"],"requestBody": {"required": true,"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserRegistration"}}}},"responses": {"201": {"description": "User created successfully","content": {"application/json": {"schema": {"$ref": "#/components/schemas/User"}}}},"400": {"description": "Bad request"},"500": {"description": "Internal server error"}}}},"/users/{userId}": {"get": {"summary": "Get user by ID","operationId": "getUserById","tags": ["User"],"parameters": [{"name": "userId","in": "path","description": "ID of the user to retrieve","required": true,"schema": {"type": "string"}}],"responses": {"200": {"description": "User found","content": {"application/json": {"schema": {"$ref": "#/components/schemas/User"}}}},"404": {"description": "User not found"},"500": {"description": "Internal server error"}}},"put": {"summary": "Update user by ID","operationId": "updateUserById","tags": ["User"],"parameters": [{"name": "userId","in": "path","description": "ID of the user to update","required": true,"schema": {"type": "string"}}],"requestBody": {"required": true,"content": {"application/json": {"schema": {"$ref": "#/components/schemas/User"}}}},"responses": {"200": {"description": "User updated successfully","content": {"application/json": {"schema": {"$ref": "#/components/schemas/User"}}}},"400": {"description": "Bad request"},"404": {"description": "User not found"},"500": {"description": "Internal server error"}}},"delete": {"summary": "Delete user by ID","operationId": "deleteUserById","tags": ["User"],"parameters": [{"name": "userId","in": "path","description": "ID of the user to delete","required": true,"schema": {"type": "string"}}],"responses": {"204": {"description": "User deleted successfully"},"404": {"description": "User not found"},"500": {"description": "Internal server error"}}}}},"components": {"schemas": {"UserRegistration": {"type": "object","properties": {"email": {"type": "string","format": "email"},"displayName": {"type": "string"}},"required": ["email","displayName"],"additionalProperties": false},"User": {"type": "object","properties": {"id": {"type": "string"},"email": {"type": "string","format": "email"},"displayName": {"type": "string"}},"required": ["id","email","displayName"],"additionalProperties": false}}}}
The great thing about Blackbird is that I was able to use its AI backed design feature to help generate the Open API specification. But that’s a story for another day.
Getting Blackbird API Platform
I’m using a Linux device to develop, so I downloaded the amd64 Linux Blackbird CLI binary, but there are binaries for both flavors of Mac as well as for Windows.
sudo curl -fL https://storage.googleapis.com/releases.datawire.io/blackbird/v0.3.0-beta/linux/amd64/blackbird -o /usr/local/bin/blackbirdsudo chmod a+x /usr/local/bin/blackbird
Next I logged in and was ready to go.
Generating My Project
At this point, I was ready to really dig in and get started. I’m now going to use the generate functionality of Blackbird to spin up my project and get the boilerplate for the service created.
blackbird code generate -t go -s user-service.json -o user-service
As this command runs, it prompts for a few details.
First up is project name. I’m keeping it simple and calling it user-service.
Enter value for variable ProjectName: user-service
Next up is the GoModule. This is the name of the module for the project. Here, I’m just following Go conventions and giving it the name of the full repo path.
Enter value for variable GoModule: github.com/kai-tillman/user-service
Now I need to enter the Go version to be set as the minimum in the module. It’s important to make sure it is at least the version of Go installed locally or older for validation to pass.
Enter value for variable GoVersion: 1.22.5
Finally, there will be a set of yes/no prompts for additional details to include such as a stubbed README or a Dockerfile that can be used with the other capabilities that Blackbird offers like Run/Debug or Deployment.
enable GitHub Actions - Test module? [y/n]: nenable Readme module? [y/n]: yenable Dockerfile module? [y/n]: y
At this point, Blackbird generates my project for me, sets up the APIs using Gorilla Mux, and stubs out all the handlers needed. Now I can focus on the important part of adding the business logic to the service without handling all the plumbing necessary to setup the project and handle API path routing.
Moving Forward
At this point, I’ve spent very little time and already have a project that is ready to go for implementation of the business logic. Here’s a quick view of what I have to start from after using Blackbird.
Project structure after using Blackbird’s code generation
And now I just need to start building out the important parts of the service off the stub created for me. Here’s a quick pick of the file I need to start from.
package apiimport ("context""github.com/rs/zerolog""net/http""os""time")// APIHandler is a type to give the api functions below access to a common logger// any any other shared objectstype APIHandler struct {// Zerolog was chosen as the default logger, but you can replace it with any logger of your choicelogger zerolog.Logger// Note: if you need to pass in a client for your database, this would be a good place to include it}func NewAPIHandler() *APIHandler {output := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}logger := zerolog.New(output).With().Timestamp().Logger()return &APIHandler{logger: logger}}func (h *APIHandler) WithLogger(logger zerolog.Logger) *APIHandler {h.logger = loggerreturn h}// Delete user by IDfunc (h *APIHandler) DeleteUserById(ctx context.Context, userId string) (Response, error) {// TODO: implement the DeleteUserById function to return the following responses// return NewResponse(204, {}, "", responseHeaders), nil// return NewResponse(404, {}, "", responseHeaders), nil// return NewResponse(500, {}, "", responseHeaders), nilreturn NewResponse(http.StatusNotImplemented, ErrorMsg{"deleteUserById operation has not been implemented yet"}, "application/json", nil), nil}// Get user by IDfunc (h *APIHandler) GetUserById(ctx context.Context, userId string) (Response, error) {// TODO: implement the GetUserById function to return the following responses// return NewResponse(200, User{}, "application/json", responseHeaders), nil// return NewResponse(404, {}, "", responseHeaders), nil// return NewResponse(500, {}, "", responseHeaders), nilreturn NewResponse(http.StatusNotImplemented, ErrorMsg{"getUserById operation has not been implemented yet"}, "application/json", nil), nil}// Register a new userfunc (h *APIHandler) RegisterUser(ctx context.Context, reqBody UserRegistration) (Response, error) {// TODO: implement the RegisterUser function to return the following responses// return NewResponse(201, User{}, "application/json", responseHeaders), nil// return NewResponse(400, {}, "", responseHeaders), nil// return NewResponse(500, {}, "", responseHeaders), nilreturn NewResponse(http.StatusNotImplemented, ErrorMsg{"registerUser operation has not been implemented yet"}, "application/json", nil), nil}// Update user by IDfunc (h *APIHandler) UpdateUserById(ctx context.Context, userId string, reqBody User) (Response, error) {// TODO: implement the UpdateUserById function to return the following responses// return NewResponse(200, User{}, "application/json", responseHeaders), nil// return NewResponse(400, {}, "", responseHeaders), nil// return NewResponse(404, {}, "", responseHeaders), nil// return NewResponse(500, {}, "", responseHeaders), nilreturn NewResponse(http.StatusNotImplemented, ErrorMsg{"updateUserById operation has not been implemented yet"}, "application/json", nil), nil}
Conclusion
And there you have it. I’m ready to go with implementing the key features of the service without worrying about the overhead of getting the project setup with an API handling framework. I definitely saved time here, and this was just for a single service. This doesn’t even factor in the time and effort saved for the other services that need to built to complete the new application I’m building.
The code generation isn’t the only useful feature of Blackbird either. As mentioned, it helped to quickly get the OpenAPI spec generated without having to spend any time in an editor. It can also be used to spin up a mock server that the rest of the team can build against while I’m working on the user service. When I’m done with implementation of the service, I can deploy the implementation out and replace the running API mocking. There’s much here that can help us in different ways to spend our valuable time more effectively.