Devot Logo
Devot Logo
Arrow leftBack to blogs

How to Build a High-Performance CRUD API with Go

Manuela T.6 min readMar 21, 2025Technology
Manuela T.6 min read
Contents:
Why Go?
Project setup
Let’s implement our first endpoint - GET All items
Let’s add Docker
Is it really that fast?
Conclusion

Why Go?

When we started thinking about building an API in Go, our main motivation was execution speed. We had heard a lot about Go being a fast, efficient, and lightweight language, so we wanted to see for ourselves how it performs when building a simple API.

Go (or Golang) is known for its simplicity, concurrency model, and performance. Unlike many other languages that rely on frameworks with heavy abstractions, Go allows you to build an API with just its standard library or minimal dependencies. This makes it an excellent choice for developers who want control, efficiency, and a clean, maintainable codebase.

So why would someone choose Go for API development? The language compiles to a single binary, making deployment incredibly easy. Its built-in concurrency support ensures APIs handle multiple requests efficiently, and thanks to its static typing and garbage collection, Go strikes a great balance between safety and speed.

In this blog, we’ll walk through building a simple CRUD API using pure Golang, without a database—just a JSON file for persistence. We’ll also integrate hot-reloading with AIR and use the Chi router to handle requests. Let’s dive in!

Project setup

Before diving into the code, let’s first establish a clean and organized folder structure. Keeping the project well-structured makes it easier to maintain and scale. Here’s how we’ve organized our Go API:

data
      items.json        # Stores our data in JSON format
      handlers
      listItems.go      # Handles fetching all items
      ...              # Other handlers for CRUD operations
      model
      item.go           # Defines the item struct
      routes
      routes.go         # Defines API routes and maps them to handlers
      services
      response.go       # Utility for sending structured API responses
      go.mod                # Go module file
      go.sum                # Dependency checksums
    main.go               # Application entry point  

This structure separates concerns effectively:

  • handlers/ contains the logic for handling API requests.

  • model/ defines data structures used across the API.

  • routes/ centralizes route definitions for better readability.

  • services/ abstracts reusable utilities.

  • data/ stores the JSON file acting as our database.

With this setup, we ensure that our Go API remains modular, clean, and scalable. Now, let's move on to building the API step by step!

Let’s implement our first endpoint - GET All items

Now that we have our folder structure set up, let’s implement our first API endpoint: fetching all items from items.json. This will involve the following files:

  1. main.go – The entry point of our application.

  2. model/item.go – Defines the Item struct.

  3. routes/routes.go – Sets up the API routes.

  4. handlers/listItems.go – Implements the logic to fetch all items.

1. Define item model

This struct will be used when encoding/decoding JSON data. The struct tags (json:"...") ensure proper mapping between JSON keys and struct fields.

2. Implementing GET handler

The handler function will read data from items.json and return it as a JSON response.

3. Setting up Routes

4. The Entry Point

It’s important to note that we use several service classes to keep our code clean and modular. For example, we use a response service to ensure that every API response—whether a success or an error—is consistently formatted as JSON (services.JsonResponse(w, errorResponse.Status, errorResponse, r)). This approach improves readability, reduces redundancy, and makes it easier to handle responses across multiple endpoints.

Suppose you want to use a custom service for JSON responses instead of the standard http.ResponseWriter, you can do so by adding a response.go file inside the services folder with the following code:

With this, you can replace all direct w.Write(response) calls in your handlers with services.JsonResponse(...), ensuring every response follows a consistent JSON structure.

Now, all that’s left is to start our API by running the following command in the terminal:

Once the server is up and running, we can test our first API endpoint using curl:

This request should return all items stored in items.json in JSON format. If everything works correctly, we have successfully set up our first Go API endpoint!

Let’s add Docker

Great! We now have a working Go API, but there’s one problem—every developer who wants to run the project needs to install Go locally and ensure they have the correct version. This can lead to inconsistencies, especially when working in a team. We don’t want to force every developer to manually install Go and manage versions.

To solve this, we’ll dockerize our API, allowing it to run in a containerized environment with all dependencies included. This ensures that everyone on the team runs the API in the same, controlled environment—no need to install Go manually. In the next step, we’ll set up a Dockerfile and configure our API to run inside a Docker container.

Before we add Docker, let’s address something that has probably been bothering you—every time we make a change, we have to manually restart the API by running go run ./main.go again. This lack of hot reload can slow down development and become frustrating over time.

Since we want to be as efficient as possible and avoid wasting time constantly stopping and restarting the application, we’ll include AIR, a package that enables hot reload for Go applications, directly in our Docker setup. This way, whenever we modify our code, the API will automatically recompile, making development much smoother. Let’s dive into setting up Docker with AIR!

1. Add air to our app

Add necessary files to enable air package

2. Add Docker setup

And, of course, we need docker-compose.yaml

Great! Now, if you run the following commands:

Your Go application will start inside a Docker container with hot reload enabled, thanks to AIR. This means that every time you make a change in your code, the API will automatically restart without needing to manually stop and restart the container.

To verify that everything is working correctly, try making some changes in your project—for example, modifying a handler or adding a new log statement. If the changes take effect immediately without requiring a manual restart, then our setup is successfully running with Docker and AIR.

Is it really that fast?

Now that we have built our basic API with a GET endpoint to fetch a list of items, we can test just how fast Go is. One of the key advantages of Go is its performance, thanks to its compiled nature and efficient concurrency handling.

To see this in action, we can check our Docker container logs, where we can log the execution time for each request. By making a request to our API and inspecting the logs, we can get a clear picture of how quickly Go processes and returns the data.

Server-first approach

As you can see, the response time is incredibly low - 1.25ms, demonstrating how well Go handles API requests, even with a lightweight setup like ours. This speed advantage makes Go an excellent choice for building high-performance backend services.

Conclusion

We have successfully built a simple CRUD API using pure Go, structured it in a clean and modular way, and then dockerized it to ensure consistency across different environments. Additionally, we integrated AIR for hot reload, making our development workflow more efficient by automatically restarting the server whenever we make changes.

Using Go for API development has several advantages:

  • Performance – Go is fast and efficient, making it ideal for high-performance APIs.

  • Simplicity – With Go’s minimalistic syntax and built-in concurrency, writing scalable applications is straightforward.

  • Portability – Thanks to Docker, our API can run anywhere without worrying about dependencies or environment differences.

  • Development speed – With AIR’s hot reload, we save time by avoiding manual restarts after every change.

However, there are also some trade-offs to consider:

  • Manual JSON handling – Unlike some frameworks, Go requires manual JSON parsing and struct handling.

  • Less Built-in convenience – Compared to frameworks like Express.js (Node.js) or Django (Python), Go’s standard library is minimal, requiring more boilerplate code.

  • Learning curve – If you’re new to Go, concepts like strict typing and Goroutines might take some time to master.

Despite these trade-offs, Go remains an excellent choice for building APIs, especially when performance, scalability, and efficiency are key factors. By combining Go with Docker and AIR, we’ve created a streamlined development process that ensures reliability and ease of use for both developers and production environments.

Spread the word:
Keep readingSimilar blogs for further insights
How to Write and Use Functions in JavaScript
Technology
Max L.8 min readMar 14, 2025
How to Write and Use Functions in JavaScriptFunctions are one of the most important concepts in JavaScript. They allow you to encapsulate logic, making your code reusable and modular. Let’s dive into how they work and explore the different types of functions.
Next.js Middleware for Beginners: How to Implement and Use It Effectively
Technology
Nikola D.5 min readMar 4, 2025
Next.js Middleware for Beginners: How to Implement and Use It EffectivelyLearn how to implement Next.js middleware for authentication, redirects, and performance optimization. A beginner-friendly guide with practical examples and best practices.
API vs UI Testing: Why a Hybrid Approach With Playwright Is the Answer
Technology
Domagoj M.6 min readJan 21, 2025
API vs UI Testing: Why a Hybrid Approach With Playwright Is the AnswerSoftware testing comes in many various types, each serving distinct purposes. Let's focus on UI and API testing and learn when to use UI testing when to rely on API testing, and how to effectively combine them with a hybrid approach.