*Best Practices for Creating a Concurrent API in Golang
Project Overview
*
In this project, we will build a concurrent bulk SMS application for a telecom company called NAYY TELCO.
The main goal of this project is to understand how Golang handles concurrency using workers, jobs, and channels, and how this approach helps in sending bulk SMS messages fast and reliably.
Project Objectives
- Send bulk SMS messages to multiple customers.
- Use worker pools to process messages concurrently.
Understand how Golang conceptualizes:
o Jobs
o Workers
o ChannelsBuild a clean API that can communicate with a React frontend.
Technology Stack
Frontend
• React.js
• Tailwind CSS
• Axios
• react-router-dom
Backend
• Golang
• net/http (manual HTTP server)
• http.ServeMux (router)
• CORS middleware
• Goroutines, Channels, and WaitGroups
Frontend Overview
The frontend is built using React.js and provides a simple user interface for sending bulk SMS messages.
The form contains four input fields:
• Sender Name – The name of the sender (e.g., NAYY TELCO)
• Recipient Name – The name of the customer
• Recipient Number – The customer’s phone number
• Message – The SMS content to be sent
import { useState } from "react";
import axios from "axios";
export default function BulkSms() {
const [from, setFrom] = useState("");
const [messages, setMessages] = useState([
{ name: "", to: "", message: "" }
]);
const [loading, setLoading] = useState(false);
const handleChange = (index, field, value) => {
const updated = [...messages];
updated[index][field] = value;
setMessages(updated);
};
const addRow = () => {
setMessages([...messages, { name: "", to: "", message: "" }]);
};
const removeRow = (index) => {
setMessages(messages.filter((_, i) => i !== index));
};
const sendMessages = async () => {
setLoading(true);
try {
await axios.post("http://localhost:8080/api/sms/send", {
messages: messages.map(m => ({
...m,
from
}))
});
alert("SMS sent successfully 🎉");
// setMessages([{ name: "", to: "", message: "" }]);
} catch (err) {
alert("Failed to send SMS");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<div className="w-full max-w-4xl bg-white rounded-xl shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-4">
📩 Bulk SMS Sender
</h2>
{/* Sender */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-1">
Sender Name
</label>
<input
type="text"
value={from}
onChange={e => setFrom(e.target.value)}
placeholder="e.g. NAYY Telco"
className="w-full border rounded-lg px-4 py-2 focus:ring focus:ring-blue-200"
/>
</div>
{/* Messages */}
<div className="space-y-4">
{messages.map((msg, index) => (
<div
key={index}
className="border rounded-lg p-4 bg-gray-50"
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<input
type="text"
placeholder="Recipient Name"
value={msg.name}
onChange={e =>
handleChange(index, "name", e.target.value)
}
className="border rounded-lg px-3 py-2"
/>
<input
type="text"
placeholder="Phone Number"
value={msg.to}
onChange={e =>
handleChange(index, "to", e.target.value)
}
className="border rounded-lg px-3 py-2"
/>
<button
onClick={() => removeRow(index)}
className="text-red-500 text-sm hover:underline"
disabled={messages.length === 1}
>
Remove
</button>
</div>
<textarea
placeholder="Message"
value={msg.message}
onChange={e =>
handleChange(index, "message", e.target.value)
}
className="w-full mt-3 border rounded-lg px-3 py-2"
rows={3}
/>
</div>
))}
</div>
{/* Actions */}
<div className="flex justify-between items-center mt-6">
<button
onClick={addRow}
className="text-blue-600 hover:underline"
>
+ Add another recipient
</button>
<button
onClick={sendMessages}
disabled={loading}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Sending..." : "Send SMS"}
</button>
</div>
</div>
</div>
);
}
Users can add multiple recipients and send all messages in one request to the backend.
Backend Structure
Key Concepts Used
• Handlers – Receive HTTP requests from the frontend.
• Channels – Used as job queues for SMS messages.
• Workers (Goroutines) – Process messages concurrently.
• WaitGroups – Ensure all messages are processed before responding.
• ServeMux – Routes HTTP requests to the correct handler.
• CORS Middleware – Allows communication between React and Go.
- *Models * MessageStruct Data
Defining the model for MessageData
Every request body is strictly define to follow this format.
Example
Name : “Nicholas”
From : “+23355762190”
To : “+124672909”
Message : “This message is a broadcast”
2. Worker
The is where the magic is , it has three parameters : id , jobs and wg
** id**
Used to uniquely identify each worker. It is mainly for logging and debugging purposes and does not affect concurrency behavior.
** jobs <-chan models.MessageData**
This is a receive-only channel that delivers jobs to the worker.
Each job must be of type MessageData, which is defined in the models package.
The channel acts as a job queue, and the worker blocks until a job is available.
** wg sync.WaitGroup*
The WaitGroup is used to synchronize the workers with the main function.
It allows the main function to wait until all workers have completed their work before exiting.
defer wg.Done()
This signals that the worker has finished execution and decreases the WaitGroup counter by one.
It does not control job assignment or worker availability.
For loop (for data := range jobs)
This loop continuously receives jobs from the channel and processes them.
The loop runs until the channel is closed and all jobs are consumed.
- Handlers
1️⃣ Request Validation
• The handler only allows POST requests.
• Any other HTTP method is rejected with a Method Not Allowed error.
2️⃣ Read Request Data
• The incoming request body is decoded from JSON.
• It expects a list of messages that follow the MessageData structure.
• If the request body is invalid, the request is rejected.
3️⃣ Create Workers and Job Queue
• A fixed number of workers (10) is created.
• A channel (jobs) is used as a job queue to distribute SMS messages.
• A WaitGroup is used to track when all workers finish processing.
4️⃣ Start Workers
• Each worker runs in its own goroutine.
• Workers continuously listen for jobs from the channel and send SMS messages.
5️⃣ Send Jobs to Workers
• Each message from the request is sent into the jobs channel.
• Workers automatically pick up messages and process them concurrently.
6️⃣ Graceful Shutdown
• The jobs channel is closed to signal that no more work is coming.
• The handler waits until all workers finish before responding.
7️⃣ Response
• Once all messages are processed, a success response is sent back to the client.
- Message ( Send Function )
What it Does
• It receives a single message of type MessageData.
• The message contains the sender name, recipient number, and message content.
• The function formats and prints the SMS details to the console.
- *Main *
Create a ServeMux
mux := http.NewServeMux()
• ServeMux is Go’s built-in HTTP request router.
• It maps URL paths to their corresponding handler functions.
• This allows clean and organized route management.
o Register API Route (Important Fix)
mux.HandleFunc("/api/sms/send", handlers.SendSMSHandler)
• Registers the SMS sending endpoint.
• Any POST request to /api/sms/send is handled by SendSMSHandler.
• This fixes the 404 error, because the route is now registered on the same mux being served.
**
o Enable CORS Middleware**
handler := middleware.EnableCORS(mux)
• Wraps the router with CORS support.
• Allows frontend apps (like React) to communicate with this backend.
• Required for browser-based API requests.
o Start the HTTP Server
http.ListenAndServe(":8080", handler)
• Starts the server on port 8080.
• Listens for incoming HTTP requests.
• Uses the CORS-enabled handler.
- CORS
- Purpose of this middleware This middleware enables CORS (Cross-Origin Resource Sharing) so that a React frontend can communicate with the Golang backend running on a different port.
- EnableCORS(next http.Handler) o Accepts another HTTP handler (next) o Wraps it with extra logic before the request reaches the actual route handler
- http.HandlerFunc Converts a function into an HTTP handler so it can intercept requests.
- Access-Control-Allow-Origin Allows requests only from: http://localhost:5173 (This is the default React development server) . Access-Control-Allow-Methods Specifies which HTTP methods are allowed: • POST • GET • OPTIONS Access-Control-Allow-Headers Allows headers commonly used in API requests, such as: • Content-Type • Authorization Handling OPTIONS requests (Preflight) • Browsers send an OPTIONS request first to check permissions • If detected, the server responds with 200 OK and stops further processing next.ServeHTTP(w, r) If the request is valid, it is passed to the actual route handler (e.g., /api/sms/send).








Top comments (0)