Build a Patient Reminder System with Go and Twilio

December 09, 2025
Written by
David Fagbuyiro
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Build a Patient Reminder System with Go and Twilio

In this tutorial, you'll learn how to build a simple yet powerful patient reminder system using Go and Twilio. The application will let users schedule medical appointments, store them in a MySQL database, and automatically send SMS reminders to patients before their appointments. You’ll create a web-based form to capture appointment details and set up a background job that checks for upcoming appointments and triggers SMS alerts using Twilio’s Programmable Messaging API.

This project is ideal for developers who want hands-on experience combining Go’s backend capabilities with real-world integrations like messaging and scheduling.

Prerequisites

  • Go version 1.22 or above
  • A Twilio account (either free or paid). If you are new to Twilio, click here to create a free account
  • Basic knowledge of Go
  • Access to a MySQL database, and some prior database experience
  • A mobile phone that can send SMS

Create a new Go application

Creating the project involves initializing a new Go module using the go mod init command. This sets up the foundational structure for your application by creating a go.mod file which defines the module’s name and tracks its dependencies. It’s how Go knows about your project and what external packages it relies on.

Installing dependencies means that any third-party packages your application imports are downloaded and properly tracked. This happens when you run go get or go build, or run your program, prompting Go to fetch the necessary packages and record them in the go.mod and go.sum files.

Run the following commands to create a project directory, navigate into it, and initialise a new Go module:

mkdir reminder && cd reminder
go mod init

Specifically, the command above creates a folder named reminder, enters it, and initializes a Go module named "github.com/<yourusername>/reminder".

Register environment variables

To simplify storing the application's configuration information, you'll use environment variables. These will be stored in a file named .env and loaded into the application's environment using GoDotEnv.

Start by creating a file named .env in the root of your project and add the following to it:

TWILIO_ACCOUNT_SID=<your_twilio_account_sid>
TWILIO_AUTH_TOKEN=<your_twilio_auth_token>
TWILIO_PHONE_NUMBER=<your_twilio_phone_number>
DB_SOURCE=root:@tcp(localhost:3306)/remindersystem

Retrieve your Twilio access credentials

Next, log in to your Twilio Console to retrieve your Account SID, Auth Token, and Twilio phone number, as you can see in the screenshot below.

Twilio account information
Twilio account information

Then, use them to replace <your_twilio_account_sid>, <your_twilio_auth_token>, and <your_twilio_phone_number>, respectively, in .env.

Set up the database

Now, login to your MySQL server and create a new database named "remindersystem". After creating the database, create a table named "patient" with the following SQL command:

CREATE TABLE IF NOT EXISTS `patient` (
  `PatientName` text NOT NULL,
  `PhoneNumber` varchar(15) NOT NULL,
  `Drugs` text NOT NULL,
  `DrugDescription` text NOT NULL,
  `AppointmentTime` datetime NOT NULL,
  `ReminderSent` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
COMMIT;

Connect to the database

To connect your Go application to the MySQL database, first, ensure that MySQL is running. This can be achieved in several ways depending on your development setup:

  • Using XAMPP or MAMP (common on Windows and macOS): Start MySQL from the XAMPP or MAMP control panel, and open phpMyAdmin by navigating to http://localhost/phpmyadmin in your browser.
  • On Linux or BSD: MySQL may be installed and managed natively using your system’s package manager (e.g., apt, yum, dnf, and pkg)
  • With containers: You can run a MySQL container, and optionally phpMyAdmin, as a separate container for database management using Docker or Podman

Then in your Go project, open .env and add the following, which defines your database DSN (Data source name):

DB_SOURCE=<username>:<password>@tcp(127.0.0.1:3306)/remindersystem?parseTime=true

Then, replace <username> and <password> with the username and password for your MySQL database.

Retrieving the Go packages

To retrieve the Go packages that will be used in the next phase, you use the go get command, which fetches and installs the specified modules along with their dependencies, by running the command below:

go get github.com/go-sql-driver/mysql github.com/joho/godotenv github.com/robfig/cron/v3 github.com/twilio/twilio-go

Go will update your go.mod and go.sum files to track these dependencies and ensure they are available during compilation and runtime.

Now, create a file named main.go inside the root of your project directory. Once you've created it, add the provided code below to the file.

package main
import (
	"database/sql"
	"html/template"
	"log"
	"net/http"
	"os"
	"time"
	_ "github.com/go-sql-driver/mysql"
	"github.com/joho/godotenv"
	"github.com/robfig/cron/v3"
	"github.com/twilio/twilio-go"
	openapi "github.com/twilio/twilio-go/rest/api/v2010"
)
type Appointment struct {
	PatientName     string
	PhoneNumber     string
	Drugs           string
	DrugDescription string
	AppointmentTime time.Time
	ReminderSent    bool
}
var db *sql.DB
func getEnv(key string) string {
	val := os.Getenv(key)
	if val == "" {
		log.Fatalf("Missing env var: %s", key)
	}
	return val
}
func connectDB() {
	var err error
	db, err = sql.Open("mysql", getEnv("DB_SOURCE"))
	if err != nil {
		log.Fatal("Failed to open DB:", err)
	}
	if err = db.Ping(); err != nil {
		log.Fatal("Failed to ping DB:", err)
	}
	log.Println("Connected to the database.")
}
func sendReminder(to, message string) error {
	client := twilio.NewRestClientWithParams(twilio.ClientParams{
		Username: getEnv("TWILIO_ACCOUNT_SID"),
		Password: getEnv("TWILIO_AUTH_TOKEN"),
	})
	params := &openapi.CreateMessageParams{}
	params.SetTo(to)
	params.SetFrom(getEnv("TWILIO_PHONE_NUMBER"))
	params.SetBody(message)
	_, err := client.Api.CreateMessage(params)
	return err
}
func scheduleReminders() {
	c := cron.New()
	c.AddFunc("@every 1m", func() {
		now := time.Now()
		oneHourLater := now.Add(time.Hour)
		rows, err := db.Query(`
			SELECT PatientName, PhoneNumber, Drugs, DrugDescription, AppointmentTime
			FROM patient
			WHERE ReminderSent = FALSE AND AppointmentTime BETWEEN ? AND ?`,
			now, oneHourLater)
		if err != nil {
			log.Println("Reminder query error:", err)
			return
		}
		defer rows.Close()
		for rows.Next() {
			var appt Appointment
			err := rows.Scan(&appt.PatientName, &appt.PhoneNumber, &appt.Drugs, &appt.DrugDescription, &appt.AppointmentTime)
			if err != nil {
				log.Println("Scan error:", err)
				continue
			}
			msg := "Hi " + appt.PatientName + ", this is a reminder for your appointment at " +
				appt.AppointmentTime.Format("3:04 PM, Jan 2.") + ". Drugs: " + appt.Drugs + ". Notes: " + appt.DrugDescription
			err = sendReminder(appt.PhoneNumber, msg)
			if err != nil {
				log.Println("Failed to send SMS to", appt.PhoneNumber, ":", err)
				continue
			}
			_, err = db.Exec("UPDATE patient SET ReminderSent = TRUE WHERE PhoneNumber = ? AND AppointmentTime = ?", appt.PhoneNumber, appt.AppointmentTime)
			if err != nil {
				log.Println("Failed to update ReminderSent:", err)
			} else {
				log.Println("Reminder sent to", appt.PhoneNumber)
			}
		}
	})
	c.Start()
}
func showAppointments(w http.ResponseWriter, r *http.Request) {
	rows, err := db.Query("SELECT PatientName, PhoneNumber, Drugs, DrugDescription, AppointmentTime, ReminderSent FROM patient ORDER BY AppointmentTime ASC")
	if err != nil {
		http.Error(w, "Query error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()
	var appts []Appointment
	for rows.Next() {
		var a Appointment
		err := rows.Scan(&a.PatientName, &a.PhoneNumber, &a.Drugs, &a.DrugDescription, &a.AppointmentTime, &a.ReminderSent)
		if err != nil {
			http.Error(w, "Scan error", http.StatusInternalServerError)
			return
		}
		appts = append(appts, a)
	}
	tmpl, err := template.ParseFiles("templates/appointments.html")
	if err != nil {
		http.Error(w, "Template error", http.StatusInternalServerError)
		return
	}
	tmpl.Execute(w, appts)
}
func newAppointmentForm(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("templates/new.html")
	if err != nil {
		http.Error(w, "Template error", http.StatusInternalServerError)
		return
	}
	tmpl.Execute(w, nil)
}
func createAppointment(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Redirect(w, r, "/appointments/new", http.StatusSeeOther)
		return
	}
	r.ParseForm()
	name := r.FormValue("patient_name")
	phone := r.FormValue("phone_number")
	drugs := r.FormValue("drugs")
	drugDesc := r.FormValue("drug_description")
	timeStr := r.FormValue("appointment_time")
	apptTime, err := time.Parse("2006-01-02T15:04", timeStr)
	if err != nil {
		http.Error(w, "Invalid time format", http.StatusBadRequest)
		return
	}
	_, err = db.Exec(`INSERT INTO patient (PatientName, PhoneNumber, Drugs, DrugDescription, AppointmentTime, ReminderSent)
		VALUES (?, ?, ?, ?, ?, ?)`, name, phone, drugs, drugDesc, apptTime, false)
	if err != nil {
		log.Println("Insert error:", err)
		http.Error(w, "Insert failed", http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/appointments", http.StatusSeeOther)
}
func deleteAppointment(w http.ResponseWriter, r *http.Request) {
	phone := r.URL.Query().Get("phone")
	timeStr := r.URL.Query().Get("time")
	apptTime, err := time.Parse("2006-01-02T15:04:05", timeStr)
	if err != nil {
		http.Error(w, "Invalid appointment time", http.StatusBadRequest)
		return
	}
	_, err = db.Exec("DELETE FROM patient WHERE PhoneNumber = ? AND AppointmentTime = ?", phone, apptTime)
	if err != nil {
		log.Println("Delete error:", err)
		http.Error(w, "Failed to delete appointment", http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/appointments", http.StatusSeeOther)
}
func main() {
	if err := godotenv.Load(); err != nil {
		log.Fatal("Error loading .env")
	}
	connectDB()          
	scheduleReminders()  
	http.HandleFunc("/appointments", showAppointments)
	http.HandleFunc("/appointments/new", newAppointmentForm)
	http.HandleFunc("/appointments/create", createAppointment)
	http.HandleFunc("/appointments/delete", deleteAppointment)
	log.Println("Server running at http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

The code above integrates with Twilio to send SMS notifications. It connects to a MySQL database and sets up a table to store appointment details, including the patient's name, phonenumber, appointment time, and a flag indicating whether a reminder has been sent.

The application serves a basic web interface with two routes, one for listing all appointments and another for adding new ones. When users request a new appointment via the form, the data is saved in the database, and the user is redirected to the appointment list.

A key feature of this system is the use of a cron job through the robfig/cron package. The job runs every hour and checks for any upcoming appointments within the next hour that haven't received a reminder. If it finds any, it sends an SMS reminder using Twilio's Programmable Messaging API and marks that appointment as reminder sent in the database. This automated scheduling allows the application to function without manual intervention, making sure patients receive timely reminders without user involvement or additional services.

Add HTML pages

Create a folder named templates in your root folder, and inside it create a file called New.html inside your templates folder to create a fresh reminder, once that is done, paste the code below in the New.html file:

<!DOCTYPE html>
<html>
<head>
    <title>Patient Reminder Form</title>
    <style>
        body {
            background-color: #f1f2f6;
            font-family: Arial, sans-serif;
            text-align: center;
            padding: 40px;
        }
        form {
            background-color: white;
            border-radius: 10px;
            box-shadow: 0px 0px 10px #ccc;
            display: flex;
            flex-direction: column;
            flex-wrap: nowrap;
            column-gap: 25px;
            row-gap: 27px;
            margin: auto;
            padding: 45px 35px 45px 35px;
            width: 400px;
        }
        input, select, textarea {
            border-radius: 5px;
            border: 1px solid #ccc;
            flex-grow: 1;
            font-size: 16px;
            padding: 10px;
        }
        button {
            background-color: #3498db;
            border-radius: 5px;
            border: none;
            color: white;
            font-size: 16px;
            padding: 10px;
        }
        h2 {
            color: #007bff;
        }
    </style>
</head>
<body>
    <h2>Patient Reminder Form</h2>
    <form action="/appointments/create" method="POST">
        <input type="text" name="patient_name" placeholder="Full name" required>
        <input type="text" name="phone_number" placeholder="Phone Number" required>
        <textarea name="drugs" placeholder="Drugs (e.g., Paracetamol, Vitamin C)" required></textarea>
        <textarea name="drug_description" placeholder="Drugs Description (e.g., 1-1-1 after meals)" required></textarea>
        <input type="datetime-local" name="appointment_time" required>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

The HTML code above creates a styled patient reminder form that allows users to submit appointment details to a Go backend via a POST request to the "/appointments/create" endpoint. The form includes fields for the patient's full name, phone number, prescribed drugs, a description of how the drugs should be taken, and the appointment time using a datetime-local input, which you can see an example of in the image below.

Web form titled Patient Reminder Form with fields for name, phone, drugs, description, and date.

The next step is for you to create a new file called appointments.html in the templates directory, then copy and paste the code below into the new file:

<!DOCTYPE html>
<html>
<head>
    <title>Patient Appointments</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; }
        table {
            border-collapse: collapse;
            margin: 20px auto;
            width: 90%;
        }
        th, td {
            border: 1px solid #ccc;
            padding: 10px;
        }
        th { background-color: #f2f2f2; }
        a {
            display: inline-block;
            margin: 20px;
            text-decoration: none;
            font-size: 18px;
        }
    </style>
</head>
<body>
    <h2>Patient Appointments</h2>
    <a href="/appointments/new">➕ Add New Appointment</a>
    <table>
        <tr>
            <th>Name</th>
            <th>Phone</th>
            <th>Appointment Time</th>
            <th>Drugs</th>
            <th>Drug Description</th>
            <th>Reminder Sent</th>
        </tr>
        {{range .}}
        <tr>
            <td>{{.PatientName}}</td>
            <td>{{.PhoneNumber}}</td>
            <td>{{.AppointmentTime.Format "2006-01-02 15:04"}}</td>
            <td>{{.Drugs}}</td>
            <td>{{.DrugDescription}}</td>
            <td>{{if .ReminderSent}}✅ Yes{{else}}❌ No{{end}}</td>
        </tr>
        {{end}}
    </table>
</body>
</html>

This HTML template displays a table of patient appointments, dynamically rendered using Go’s html/template package, with data passed from the backend. It includes a heading, a link to add a new appointment, and a styled table listing each patient’s name, phone number, appointment time formatted, prescribed drugs, drug description, and whether an SMS reminder has been sent.

The range loop iterates through all appointments provided by the server, inserting them as table rows, and conditionally shows a checkmark or cross based on the "ReminderSent" status.

Patient appointment management screen showing appointment details and reminder status.

You now have a complete and working SMS reminder system that uses Twilio to send notifications. The backend, built with Go and connected to a MySQL database, supports creating and viewing patient appointments through a clean, styled HTML interface. A cron job runs automatically every minute (ideal for development and testing) to check for upcoming appointments and send reminders when necessary.

Test the application

With everything set up, you can begin testing SMS delivery by scheduling appointments within the next hour to trigger reminder messages.

To do so, you first need to start the application development server. To do that, open another terminal tab or window and run the command below:

go run main.go

Finally, open http://localhost:8080/appointments/new in your browser of choice, to create a reminder. Once that is done, you will be directed to a status page where you will see if the reminder has been sent to your registered phone number or not.

Check your Twilio registered phone number and you'll see the reminder details and description right in your SMS, similar to the screenshot below.

Reminder received as sms
Reminder received as sms

That's how to build a patient reminder system with Go and Twilio

You’ve set up a backend with Go, connected it to a MySQL database managed through phpMyAdmin, created a web interface for scheduling appointments, and integrated Twilio to send automated SMS reminders. With the addition of a cron job, your system can now run checks in the background and notify patients without manual input.

This setup is lightweight, efficient, and easy to expand whether you want to add email notifications, authentication, or a full admin dashboard in the future.

David Fagbuyiro is a software engineer and technical writer who is passionate about extending his knowledge to developers through technical writing. You can find him on LinkedIn.