Building a Simple Contact Form API Using Golang, Gin Framework, GORM, MariaDB, and Docker

Introduction

πŸ’‘

This article has a related root article:

Building a Full-Stack Simple Contact Form: Integrating API, Web Interface, CMS, and Database with Docker Compose Multi-Container Architecture

Contact forms play a crucial role in online communication, linking users with service providers effectively. Whether it's for gathering feedback, handling inquiries, or offering support.

In this tutorial, we will demonstrate how to build a simple Contact Form API using Golang, Gin, GORM, and Docker. This technology stack provides excellent performance, scalability, and simplifies both deployment and maintenance.

Project Overview

Our project, named api-contact-form, is a RESTful API that enables users to submit, retrieve, update, and delete contact messages. Leveraging Golang as main codebase, Gin framework, GORM as the ORM for database interactions, and Docker for containerization. This API is designed to be efficient, scalable, and easy to deploy across various environments.

Key Features

Prerequisites

Before getting started, make sure you have the following installed and a basic understanding of each:

Step 1: Setting Up the Project

1.1. Create the Project Directory:

Begin by creating a new directory for your project and navigating into it.

mkdir api-contact-form
cd api-contact-form

1.2. Initialize the Go Module:

Initialize a new Go module to manage your project's dependencies.

go mod init api-contact-form

1.3. Install Necessary Dependencies

Install the required Go packages for building the API. This includes the Gin web framework, GORM for ORM functionalities and other dependencies.

go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
go get -u github.com/gin-contrib/cors
go get -u github.com/joho/godotenv

Running these commands will update the go.mod and go.sum files with the necessary dependencies.

Or, you can update manually on:


Step 2: Understanding the Directory Structure

A well-organized project structure enhances maintainability and scalability. Here's an overview of the api-contact-form project's file structure:

πŸ“‚Β api-contact-form/
β”œβ”€β”€ πŸ“‚Β config/
β”‚   β”œβ”€β”€ πŸ“„ database.go
β”‚   └── πŸ“„ env.go 
β”œβ”€β”€ πŸ“‚Β handlers/
β”‚   β”œβ”€β”€ πŸ“„ contact_handler.go
β”‚   β”œβ”€β”€ πŸ“„ health_handler.go
β”‚   └── πŸ“„ main_response.go
β”œβ”€β”€ πŸ“‚Β helpers/
β”‚   β”œβ”€β”€ πŸ“„ datetime_helper.go
β”‚   └── πŸ“„ env_helper.go
β”œβ”€β”€ πŸ“‚Β models/
β”‚   └── πŸ“„ contact.go
β”œβ”€β”€ πŸ“‚Β repositories/
β”‚   └── πŸ“„ contact_repository.go
β”œβ”€β”€ πŸ“‚Β requests/
β”‚   └── πŸ“„ contact_request.go
β”œβ”€β”€ πŸ“‚Β responses/
β”‚   └── πŸ“„ api_response.go
β”œβ”€β”€ πŸ“‚Β services/
β”‚   └── πŸ“„ contact_service.go
β”œβ”€β”€ πŸ“„ .dockerignore
β”œβ”€β”€ πŸ“„ .env.example
β”œβ”€β”€ πŸ“„ .gitignore
β”œβ”€β”€ πŸ“„ docker-compose.yaml
β”œβ”€β”€ πŸ“„ Dockerfile
β”œβ”€β”€ πŸ“„ go.mod
β”œβ”€β”€ πŸ“„ go.sum
└── πŸ“„ main.go

Codebase structure mapping:


Step 3: Create Configuration Code

Database Configuration

Manages the database connection setup using GORM. This file initializes the connection to the database based on environment configurations.

Environment Variable

Handles environment variable loading and management. It ensures that the application correctly reads and utilizes configuration settings from the .env file.

Step 4: Setting up .env

Proper configuration is essential for the smooth operation of your application. We'll use environment variables to manage configurations, making it easy to switch settings across different environments (development, staging, production).

Create an .env File

Start by creating a .envfill in the necessary configurations, such as database credentials, API settings, and other environment-specific variables.

# Application Configuration
APP_PORT=8080

# Timezone Configuration
APP_TIMEZONE=Asia/Jakarta

# CORS Configuration
CORS_ALLOWED_ORIGINS=http://localhost:8081,http://localhost:8082,http://cms-contact-form:8081,http://client-contact-form:8082
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Origin,Content-Type,Accept,Authorization
CORS_ALLOW_CREDENTIALS=true
CORS_EXPOSE_HEADERS=Content-Length,Content-Type

# Database Configuration
DB_HOST=mariadb-contact-form
DB_PORT=3306
DB_USER=user
DB_PASSWORD=password
DB_NAME=contactsdb

##
## THIS CONFIG FOR DOCKER-COMPOSE.YAML ONLY, NOT FOR THE APP
## 
# Database Configuration
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=contactsdb
MYSQL_USER=user
MYSQL_PASSWORD=password

# Port Mapping Configuration
HOST_MARIADB_PORT=3306
CONT_MARIADB_PORT=3306

HOST_PHPMYADMIN_PORT=8011
CONT_PHPMYADMIN_PORT=80

HOST_API_PORT=8080
CONT_API_PORT=8080

Secure the .env File:

Ensure that the .env file is not committed to version control by verifying that it's listed in .gitignore.

# Environment variables
.env

# OS generated files
.DS_Store
Thumbs.db

# app build result
api-contact-form

Step 5: Implementing the Handlers

Handlers are responsible for processing incoming HTTP requests, interacting with services, and returning appropriate responses.

Contact Handler

Manages endpoints related to contact operations, such as creating, retrieving, updating, and deleting contacts.

Health Handler

Provides a simple health check endpoint to verify if the API is running correctly.

Main Handler

Provides default API response if the API is running.


Step 6: Defining Models and Repositories

Models represent the data structures, while repositories handle data persistence and retrieval.

Contact Model

Defines the Contact struct, representing a contact message with fields like ID, Name, Email, Phone, Message, DeletedAt, CreatedAt, and UpdatedAt. This model maps to the database table and includes GORM tags for ORM functionalities, including soft deletes.

Contact Repository

Implements the repository pattern, providing methods to interact with the database for CRUD operations related to contacts. This abstraction allows the service layer to interact with the data layer without worrying about the underlying database logic.


Step 7: Setting Up Services

Services contain the business logic, acting as an intermediary between handlers and repositories.

Contact Service

Provides methods to manage contacts, such as retrieving all contacts, fetching a contact by ID, creating a new contact, updating existing contacts, and deleting contacts.


Step 8: Creating Requests and Responses

To ensure proper data validation and consistent API responses, we'll define request and response structures.

Contact Request

Defines the structure of incoming request payloads for contact-related operations, ensuring that data is validated before processing.

API Response

Structures the API's response format, encapsulating status codes, messages, and data payloads for uniformity across all endpoints.


Step 9: Configuring Helpers

Helper functions streamline repetitive tasks, such as environment variable retrieval and date-time formatting.

DateTime Helpers

Contains functions for handling date and time operations, ensuring that timestamps are consistently formatted across the API.

Environment Helpers

Provides utility functions to retrieve environment variables with default values, simplifying configuration management.


Step 10: Writing main.go

The main.go file serves as the entry point of the application. It initializes configurations, sets up routes, and starts the server.

Step 11: Build and Run Application

Before containerizing the application with Docker, it’s important to build and run it locally to ensure everything works as expected.

Installing Dependencies

Ensure that all necessary Go dependencies are installed. Run the following command to download the dependencies specified in go.mod:

go mod tidy

This command updates the go.sum file with the checksums of the dependencies.

Building the Application

Compile the Go application by running

# this will generate binary file with filename following folder project name (api-contact-form)
go build 

or 

# if you want ot create binary with custom filename you can use -o parameter
go build -o api-contact-form .

This command builds the application and outputs an executable named main.

Running the Application

Start the application locally using:

./api-contact-form

πŸ’‘

Confirm that MariaDB is up and running, ensure all required databases have been created, and check that your environment settings properly support database connections.

Step 12: Testing the API

With the application running inside Docker containers, you can now test the API endpoints using tools like Postman, cURL, or Insomnia.

Health Check

Verify that the API is operational.

curl --location 'http://localhost:8080/health'

Expected Response:

{
    "code": "SUCCESS",
    "message": "API is running.",
    "data": null
}

Create Contact

Submit a new contact message.

curl --location 'http://localhost:8080/contacts' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name": "John Doe",
  "email": "john@example.com",
  "phone": "1234567890",
  "message": "Hello, World!"
}'

Expected Response:

{
    "code": "CREATED",
    "message": "Contact created successfully",
    "data": {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com",
        "phone": "1234567890",
        "message": "Hello, World!",
        "created_at": "2024-10-28 17:09:13",
        "updated_at": "2024-10-28 17:09:13"
    }
}

Get All Contacts

Fetch all submitted contacts.

curl --location 'http://localhost:8080/contacts/'

Expected Response:

{
    "code": "SUCCESS",
    "message": "Contacts retrieved successfully",
    "data": [
        {
            "id": 1,
            "name": "John Doe",
            "email": "john@example.com",
            "phone": "1234567890",
            "message": "Hello, World!",
            "created_at": "2024-10-28 17:09:13",
            "updated_at": "2024-10-28 17:09:13"
        }
    ]
}

Get Detail Contact

Show contact details.

curl --location 'http://localhost:8080/contacts/1'

Expected Response:

{
    "code": "SUCCESS",
    "message": "Contact retrieved successfully",
    "data": {
        "id": 4,
        "name": "John Doe",
        "email": "john@example.com",
        "phone": "1234567890",
        "message": "Hello, World!",
        "created_at": "2024-10-28 17:09:13",
        "updated_at": "2024-10-28 17:09:13"
    }
}

Update Contact

Modify an existing contact's details.

curl --location --request PUT 'http://localhost:8080/contacts/1' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "0987654321",
  "message": "Updated Message"
}'

Expected Response:

{
    "code": "SUCCESS",
    "message": "Contact updated successfully",
    "data": {
        "id": 1,
        "name": "Jane Doe",
        "email": "jane@example.com",
        "phone": "0987654321",
        "message": "Updated Message",
        "created_at": "2024-10-24 10:57:08",
        "updated_at": "2024-10-28 17:10:13"
    }
}

Delete Contact

Perform a soft delete on a contact.

curl --location --request DELETE 'http://localhost:8080/contacts/1'

Expected Response:

{
    "code": "SUCCESS",
    "message": "Contact deleted successfully",
    "data": null
}

Note: The contact is soft-deleted, meaning it's marked as deleted in the database but not permanently removed.


Step 13: Dockerizing the Application

Containerizing your application ensures consistency across different environments and simplifies deployment.

Dockerfile

Contains instructions to build the Docker image for the Contact Form API. It typically includes steps like setting the base image, installing dependencies, copying source code, building the application, and specifying the command to run the application.

.dockerignore

Specifies files and directories to exclude from the Docker build context, optimizing build performance and enhancing security by preventing unnecessary or sensitive files from being included in the Docker image.

Docker Compose

Orchestrates multi-container Docker applications, managing services like the API and the database seamlessly. It defines how containers should be built, their configurations, networks, and volumes.


Step 14: Building and Running the Containerized Application

With the configuration and setup complete, it's time to build and run your application.

Building the Docker Image

Navigate to the project root directory and build the Docker image using Docker Compose.

docker compose build

Run multiple Docker Container

After that run container using this command:

docker compose up -d

Verifying Container Status

Ensure all containers are up and running without issues.

docker compose ps

You should see entries for the API and the database services, both marked as Up.


Conclusion

Congratulations! πŸŽ‰ You've successfully built a Contact Form API using Golang, GORM, MariaDB, and Docker. This API is capable of handling contact submissions efficiently, ensuring data integrity and providing a seamless experience for both users and service providers. By containerizing the application with Docker, you've set the foundation for easy deployment and scalability, making it adaptable to various environments and workloads.


Suggested Next Steps for Improvement

To further enhance your Contact Form API, consider implementing these improvements:

Authentication and Authorization:

Logging and Monitoring

API Documentation

Unit Testing

CI/CD Pipelines

Scalability Enhancements

By continuously iterating and enhancing your API, you ensure that it remains robust, secure, and adaptable to evolving requirements and technologies.


References