Building an API with Golang, Axiom and Railway

Table of contents
- Purpose of the Project
- Prerequisites
- Creating our API
- Setting up Automatic Deployments to Railway
- Creating our Database
- Connecting to our Database
- Creating our Data Model and Database Tables
- Creating our Routes to add and Get our Todos
- Testing with Insomnia
- Create Your Axiom Dataset
- Setting Up Axiom in our API
- Logging our Events to Axiom
- Building an Axiom Dashboard to View Our Data
- Next Steps
- Completed Project

I wanted to put together a simple API tutorial using Golang, that is hosted on Railway. and uses Axiom for event logging. Below, you will find a step-by-step guide to creating a simple API. We will also be testing our API using the Insomnia app.
Purpose of the Project
The purpose of the project is pretty simple, to highlight the various services that can be utilized to achieve a well-rounded, and efficient developer experience, from code to deployment.
Prerequisites
Since the application we will be building is relatively simple, the prerequisites required to get up and running are quite minimal.
Go
Git
Insomnia (API Testing Application)
Axiom Account
Railway Account
Creating our API
Let's start by creating a folder on your desktop, or wherever you store your code. This folder will be the home of our API and our project. Remember to replace <username>
and <project>
with your GitHub username and project name respectively.
go mod init github.com/<username>/<project>
The above command will create a go.mod
file in your project directory. Let's open the project in your favorite editor. We need to add Fiber to our project. This is as easy as running a simple command.
go get -u github.com/gofiber/fiber/v2
This will download all the modules that are required to make Fiber run inside of our project. This will also create a new file in our project called go.sum
This is a checksum file that automatically updates with our dependencies.
Let's create a main.go
file and build a simple Hello World API, to make sure everything is set up properly.
// main.go
package main
import "github.com/gofiber/fiber/v2"
func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Listen(":3000")
}
This code sets up our Fiber app and creates a route on the root endpoint /
, which will return Hello, World!
In the browser when we navigate to localhost:3000
. The last line of the code is, app.Listen(":3000")
tells Fiber to start a new web server and listen for connections on port 3000.
To run your new API code, you need to run the following command in your terminal.
go run main.go
When the application is running, you will see the following displayed in your terminal.
Now you can visit http://127.0.0.1:3000
in your browser, and you will see the following.
Congratulations! Your API is officially running. In the next section, we will deploy the API code to Railway, where we will be able to access it from any web browser on any computer, not just from our development machine.
Setting up Automatic Deployments to Railway
Once you have set up your Railway account, from your dashboard, you will want to click the "New Project" button, and you will see the screen below.
You will select Deploy from GitHub repo
as long as you have your GitHub account connected. You will then select your API GitHub repository. Railway will take care of everything else. Once your project deploys, you will want to click on your service.
Since our code specifies that we are listening on port 3000, we need to set an environment variable for our port. Click on your service, then "Variables" and then "Add Variable" You will enter your new Port variable like so. Once you click the Add button, the project will redeploy.
Now to be able to access our freshly deployed API, we must generate a domain name. From the same screen, select "Settings" and then "Generate Domain".
Once you click the button, you will be given a domain name, wait a few minutes for the DNS records to propagate, and you should now be able to access your API from your newly hosted domain.
Creating our Database
On Railway, you will want to create a Postgres database. To do this, you will right-click anywhere on your canvas, you will be presented with the following widget.
Select "Database" and then "Postgres".
After a few seconds, a new service will pop up on your canvas for your database. Give this a few moments to initialize and launch. Once your Postgres instance has fully launched, we need to add a reference variable for our database connection to our API.
To do so, let's click on our API service, and then Variables. You will see your existing PORT
variable and a link that says, "Add a Variable Reference" Clicking on this link will give you a dropdown of all the shared variables from your database. You want to select, "Database URL" followed by the Add button. This adds our new variable and redeploys our existing API code.
With these changes, we can now connect our code to our database, and start building out our schema.
Connecting to our Database
Now that we have our basic Hello World API built and deployed, we are going to expand its functionality a bit. Just seeing Hello World isn't very productive. We are going to add database support. For this example, we will be using Postgres.
For the database connectivity and operations we are going to use the Gorm ORM package. The Gorm ORM can connect to a variety of databases by only changing the connection and driver used. As stated above, we will be using Postgres. To add Gorm and the Postgres driver to our project, let's add some more Go modules. Run the following commands inside your project folder using your terminal.
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
Once we have added our new Go modules to our project, we can update our code to use them. We need to create a function that will handle creating the database connection.
// main.go
package main
import (
"os"
"github.com/gofiber/fiber/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDB() {
dsn := os.Getenv("DATABASE_URL")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("Failed to connect to database")
}
DB = db
}
func main() {
app := fiber.New()
ConnectDB()
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Listen(":3000")
}
We introduced a new module os
to our code above. This allows us to get environment variables for sensitive information. We never want to store sensitive information like database connection passwords in our code or repository. We also added our new ConnectDB()
function into our main function to ensure that it is called and our database can be connected when the application runs.
Creating our Data Model and Database Tables
Now that we have our database and our code can connect to it, we need to define a model for our Todo list application, so we know what data fields are needed.
Let's create a new data type for our Todo list items.
// main.go
...
var DB *gorm.DB
type Todo struct {
ID uint `json:"id"`
Title string `json:"title"`
}
func ConnectDB() {
...
By creating our structure for the Todo type we are defining the data that will be used in our database. The type struct that we added above will migrate to our database by modifying our main function, by adding the following line below our ConnectDB()
call in our main function.
// main.go
...
func main() {
app := fiber.New()
ConnectDB()
// Migrate the database
DB.AutoMigrate(&Todo{})
...
Now when you commit your code changes and push them to GitHub, Railway will automatically detect your code changes and deploy your application again, migrating your database (creating your tables). Once your project deploys, you can select the Database tile from your project canvas, and select the "Data" tab, to reveal your new table.
Creating our Routes to add and Get our Todos
Let's add a few new routes to read and write our data to the database.
I will explain the code afterward.
// main.go
...
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Get("/todos", func(c *fiber.Ctx) error {
var todos []Todo
DB.Find(&todos)
return c.JSON(todos)
})
app.Post("/todos", func(c *fiber.Ctx) error {
var todo Todo
if err := c.BodyParser(&todo); err != nil {
return err
}
DB.Create(&todo)
return c.JSON(todo)
})
...
In the above code, we added two new routes to our API. A GET
request to the endpoint /todos
and a POST
request to the same /todos
endpoint.
The code inside of the GET
route works by creating an array of Todos that we retrieve from our database. We then write them back to the user as a JSON object.
Inside of our POST
route, we do a bit more work. Here we need to check if we received a to-do in JSON format from the request. If we didn't, we return an error. If we did receive the JSON object, we may add it to the database, and then return it to the user showing that it was successfully added.
Let's deploy our code with another git push, and we will test our new API endpoints using Insomnia in the next section.
Testing with Insomnia
To begin, we need to make sure we have downloaded and installed Insomnia. This process can be done from their website or through your operating systems package manager or app store.
You can continue to use Insomnia with the local scratchpad for the remainder of this tutorial.
In the far left column, we want to click on the small plus and add a hew HTTP Request to test. By default when creating a new endpoint, it will be a GET
request, which is perfect for what we are doing.
Once you create a new HTTP Request, we want to set it to the default URL for our project. This will be our Railway-provided domain name. We will then click the "Send" button, and in the right-hand panel, we will see that the request processed with a status of "200 OK" and will display "Hello, World!".
Now that we know our API is accessible from Insomnia, we can add another HTTP Request just like we did for the bare domain. This time, we will add our /todos
endpoint to the end of the URL. We should receive a Status 200 OK, and an empty JSON object when we hit "Send" since there is currently nothing in our database.
Since we can't really tell if our API is reading from the database, let's create a new POST request for our /todos
endpoint and try passing it some JSON data for our newest todo item.
Create a new HTTP Request. Next to the domain name, select the drop-down which will provide you with a list of all the HTTP Request types.
Once you select the POST
type, we will add a JSON body to the request, which can be done by selecting the Body type below the domain name. You will want to select JSON.
At this point, we need to add our JSON body, which will be as follows.
{
"id": 1,
"title": "This is my first todo"
}
When you hit the "Send" button now, you will see the same JSON body that you are sending appear in the right column with a status 200 OK.
This confirms that the code we wrote in our route works. However we still need to confirm that the data has been added to our database.
Back on Railway, select your Postgres service, and select the "Data" tab. You will see your todo
table listed. Click on the table, and you will be able to see the data stored inside. It should look like the following image.
In the following section, we will set up the Axiom module to start logging when our Todo endpoints are reached.
Create Your Axiom Dataset
From within your Axiom dashboard, you will want to select, "Datasets" from the main menu followed by the "New Dataset" button.
You will give your dataset a name and a description if you want through the slide-out tray on the right-hand side of your screen.
Once your dataset is created, you will be presented with a screen that has logos for all the SDKs and other integrations. You will want to click on "Create a new token" and copy the token somewhere safe.
You will also need your organization ID, which can be found in your address bar between the /
following axiom.co
. We need to add these two keys to our Railway environment variables.
Setting Up Axiom in our API
To start using Axiom to log our events, we must add the axiom-go module to our project. This can be done by running the following command in your project directory from your terminal.
go get github.com/axiomhq/axiom-go/axiom
Now that we have the axiom-go package installed, we can initialize the axiom client in our code.
// main.go
...
var DB *gorm.DB
var AXIOM *axiom.Client
...
func CreateAxiomClient() {
AXIOM_TOKEN := os.Getenv("AXIOM_TOKEN")
AXIOM_ORG_ID := os.Getenv("AXIOM_ORG_ID")
client, err := axiom.NewClient(
axiom.SetPersonalTokenConfig(AXIOM_TOKEN, AXIOM_ORG_ID),
)
if err != nil {
panic("Could not create Axiom client")
}
AXIOM = client
}
func main() {
app := fiber.New()
ConnectDB()
CreateAxiomClient()
...
At this point, you can save and commit your code to GitHub, which will trigger a new deployment on Railway. As long as your logs are clear of errors, everything is working as expected.
Logging our Events to Axiom
To begin logging an event with Axiom, we want to modify our GET
route to send logging information to Axiom when the event of retrieving our todos is triggered.
// main.go
package main
import (
"context"
"log"
"os"
"time"
"github.com/axiomhq/axiom-go/axiom"
"github.com/axiomhq/axiom-go/axiom/ingest"
"github.com/gofiber/fiber/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
var AXIOM *axiom.Client
type Todo struct {
ID uint `json:"id"`
Title string `json:"title"`
}
func ConnectDB() {
dsn := os.Getenv("DATABASE_URL")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("Failed to connect to database")
}
DB = db
}
func CreateAxiomClient() {
AXIOM_TOKEN := os.Getenv("AXIOM_TOKEN")
AXIOM_ORG_ID := os.Getenv("AXIOM_ORG_ID")
client, err := axiom.NewClient(
axiom.SetPersonalTokenConfig(AXIOM_TOKEN, AXIOM_ORG_ID),
)
if err != nil {
panic("Could not create Axiom client")
}
AXIOM = client
}
func main() {
app := fiber.New()
dataset := os.Getenv("AXIOM_DATASET")
if dataset == "" {
log.Fatal("AXIOM_DATASET is required")
}
ConnectDB()
CreateAxiomClient()
ctx := context.Background()
// Migrate the database
DB.AutoMigrate(&Todo{})
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Get("/todos", func(c *fiber.Ctx) error {
var todos []Todo
DB.Find(&todos)
// Log Axiom event
_, err := AXIOM.IngestEvents(ctx, dataset, []axiom.Event{
{ingest.TimestampField: time.Now(), "GET": "retrieved todos"},
})
if err != nil {
log.Fatalln(err)
}
return c.JSON(todos)
})
...
The above code does quite a bit, so let's cover it pretty quickly. First, we are creating a global axiom client variable. This is accessible throughout our entire application. The CreateAxiomClient()
function sets up our actual client that will be used for interacting with the Axiom API. We define the dataset that we created in the previous section. We create an ingest event that we can send our data through to store our count in Axiom.
Let's do the same thing but for our POST
method.
// main.go
app.Post("/todos", func(c *fiber.Ctx) error {
var todo Todo
if err := c.BodyParser(&todo); err != nil {
return err
}
DB.Create(&todo)
// Log Axiom event here
_, err := AXIOM.IngestEvents(ctx, dataset, []axiom.Event{
{ingest.TimestampField: time.Now(), "POST": "todo created"},
})
if err != nil {
log.Fatalln(err)
}
return c.JSON(todo)
})
You can now try creating some new todo's using your POST
method inside of Insomnia, and start collecting event data inside of Axiom.
Building an Axiom Dashboard to View Our Data
Collecting data is great, but what good is it if you can't visualize it? Let's navigate to our Axiom dashboard, and select, "Dashboards" from the main navigation. On the right-hand side of the screen, you will see a "New dashboard" button.
Let's create a new dashboard and give it a name. You are now presented with the Dashboard builder. Let's start by adding a new chart.
For this simple Dashboard, we just want to display a count of how many times each event takes place. So let's select, "Simple Query Builder".
We will want to give our new chart a name like Retrieved To-Dos
, a chart type of Statistic
select a theme and leave the Line Chart disabled. For our query, we want to make sure we are on our active dataset, visualize count(*)
and filter GET exists
. We will see a preview of the chart on the right-hand side after clicking, "Run query".
Let's save this chart, and repeat the steps above for the POST
method. Once you are done adding the POST
method chart, you will be presented with our very simple example of building a Dashboard. Don't forget to click the "Save" button at the bottom of the screen.
There are a few options in the top right of your dashboard, that you can change how often it will refresh the charts and the data. The sky is the limit to how you can visualize your data with a dashboard.
Next Steps
Now that you are armed with the basic knowledge of creating an API using Golang, creating a database and deploying your application to Railway, and have learned how to interact with Axiom, you can expand your project to handle much more complex features and requests.
Completed Project
To view the code for the completed project, you can view the repository here.
Subscribe to my newsletter
Read articles from Vincenzo Fehring directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
