Skip to main content

Make a Sandwitch

What order should one make the sandwitch to be the best practices?​

Use inside out approach:

lettuce (model) --> meat (service) --> waiter (handler) --> bread (main)
  1. The Lettuce: model.go (The Definition) Why first? You need to know what a "Product" looks like before you can fetch it or serve it. This is your "blueprint." Without this, the Chef and the Waiter don't speak the same language.

  2. The Meat: product_service.go (The Logic) Why second? This is your core business value. By building the Service next, you can actually test your database connection and SQL queries in a simple test file or a temporary main.go print statement before you ever worry about web browsers or JSON.

  3. The Waiter: handler.go (The Interface) Why third? Now that you have a "Chef" (Service) that can produce "Meat" (Data), you can write the Handler. The handler's only job is to translate the Chef's work into a "JSON plate" for the customer.

  4. The Bread: main.go(The Assembly) Why last? main.go is the glue. It's much easier to write the assembly code when all the parts (Service and Handler) are already sitting on the counter ready to be plugged in. This is also where you "open the doors" by starting the server on 8080.

Overview: Two Steps​

Component,Restaurant Role,Technical Responsibility main.go,The Building/Owner,"Starts the server, sets the port (8080), and opens the doors." handler.go,The Waiter,"Greets the customer, takes the URL request, and serves the JSON plate." product_service.go,The Chef (The Meat),Talks to the Database and prepares the raw data. model.go,The Garnish (The Lettuce),"Defines what a ""Product"" looks like so it's presentable." SQL Database,The Pantry,Where all the raw ingredients (data) are stored.

Key Code Transitions In the "Serve" step, the code undergoes two major changes:

Handler Creation: A new handler struct is created that holds the database connection and implements the ServeHTTP interface.

Server Startup: Instead of just printing products and exiting, main.go is updated to include:

Go log.Println("Server is running on port 8080...") log.Fatal(http.ListenAndServe(":8080", nil))

Let's Review File Level Functions and Folder Structure​

Folder Structure​

dba-northwind/
├── go.mod
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ └── handler/
│ └── product.go
│ └── productservice/
│ └── product_service.go
├── model/
│ └── product.go
└── README.md

Create Product Service (Meat)​

package productservice

import (
"database/sql"
"project-northwind/model" // Ensure this matches your model's import path
)

// ProductService represents the 'Chef' who interacts with the 'Pantry' (DB)
type ProductService struct {
db *sql.DB
}

// NewProductService creates a new instance of the service
func NewProductService(db *sql.DB) *ProductService {
return &ProductService{db: db}
}

// GetProducts fetches the raw ingredients (data) from the database
func (s *ProductService) GetProducts() ([]model.Product, error) {
// The SQL query is the "recipe"
rows, err := s.db.Query("SELECT productid, productname, unitprice FROM products")
if err != nil {
return nil, err
}
defer rows.Close()

var products []model.Product

// Loop through the results and fill our list
for rows.Next() {
var p model.Product
if err := rows.Scan(&p.ProductID, &p.ProductName, &p.UnitPrice); err != nil {
return nil, err
}
products = append(products, p)
}

return products, nil
}

Add a Model (Lettuce)​

The lettuce is the garnish that makes the data presentable and structured.

package model

// Product represents the 'Lettuce' - it makes the data presentable and structured
type Product struct {
ProductID int `json:"productid"`
ProductName string `json:"productname"`
UnitPrice float64 `json:"unitprice"`
}

Where these values come from?​

They are the reflection of the columns in the Products table in the Northwind database.

alt text

Event Trigger: An Order is Placed (prepare Handler.go to handle incoming HTTP requests)

Step 3: Serve the Sandwitch (API endpoints are created and ready to serve incoming HTTP requests)

Handler.go​

package handler

import (
"encoding/json"
"net/http"
"project-northwind/internal/productservice"
)

// The Handler (Waiter) now has a direct link to the ProductService (Chef)
type Handler struct {
service *productservice.ProductService
}

// NewHandler initializes the Waiter with their Chef
func NewHandler(s *productservice.ProductService) *Handler {
return &Handler{service: s}
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Routing logic: If the order is for "/products", call the right function
if r.Method == http.MethodGet && r.URL.Path == "/products" {
h.getProducts(w, r)
return
}
http.NotFound(w, r)
}

func (h *Handler) getProducts(w http.ResponseWriter, r *http.Request) {
// The Waiter just asks the Chef for the data
products, err := h.service.GetProducts()
if err != nil {
http.Error(w, "Error fetching products", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(products)
}

Check-in Point (Terminal Output)

Run the application to see product data printed in the terminal.

The handler file is not yet needed since we are not handling any incoming HTTP request (No web API) yet since no order from a customer has come in.

alt text

Handler.go

The file created in this step is handler.go file, which is the front-door handlers for incoming HTTP requests that interact with the database and return appropriate HTTP responses. They live inside the internal folder. What about routers? The main.go file will also be updated to route incoming HTTP requests to the appropriate handlers.

handler.go

package handler
import (
"database/sql"
"encoding/json"
"net/http"
"project-northwind/internal/productservice"
)

type Handler struct {
db *sql.DB
}

func NewHandler(db *sql.DB) *Handler {
return &Handler{db: db}
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/products" {
h.getProducts(w, r)
return
}
http.NotFound(w, r)
}

func (h *Handler) getProducts(w http.ResponseWriter, r *http.Request) {
productService := productservice.NewProductService(h.db)
products, err := productService.GetProducts()
if err != nil {
http.Error(w, "Error fetching products", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(products)
}

Main.go​


package main

import (
"database/sql"
"fmt"
"log"
"net/http"
"os"

"project-northwind/internal/handler"
"project-northwind/internal/productservice"

// Use the azuread driver for Entra authentication
_ "github.com/microsoft/go-mssqldb/azuread"
)

var (
db *sql.DB
server = os.Getenv("DB_SERVER")
database = os.Getenv("DB_NAME")
userid = os.Getenv("AZURE_USER_ID") // For local development
connString string
)

func main() {
// 1. Configure the Connection String
if os.Getenv("WEBSITE_INSTANCE_ID") != "" {
// Azure environment
connString = fmt.Sprintf("server=%s;database=%s;fedauth=ActiveDirectoryMSI;encrypt=true", server, database)
} else {
// Local environment (uses DefaultAzureCredential/browser login)
connString = fmt.Sprintf("server=%s;database=%s;fedauth=ActiveDirectoryDefault;user id=%s;encrypt=true", server, database, userid)
}

// 2. Open the Database Connection
var err error
db, err = sql.Open("azuresql", connString)
if err != nil {
log.Fatal("Error creating connection pool: ", err.Error())
}
defer db.Close()

// 3. Verify Connection
err = db.Ping()
if err != nil {
log.Fatal("Connection failed! Check Azure Firewall or Entra permissions: ", err.Error())
}
log.Println("Successfully connected to Azure SQL!")

// 4. Assemble the Sandwich
// Create the Chef (Service)
ps := productservice.NewProductService(db)

// Create the Waiter (Handler) and give them the Chef
h := handler.NewHandler(ps)

// 5. Start the Server (The Shop)
http.Handle("/", h)

log.Println("Server is running on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}

Where the Routing happens​

  • Registration (main.go): You tell the application to send all incoming traffic to your handler: http.Handle("/", h)

server, waiter (handler.go): Inside the ServeHTTP function in handler.go, you check the "Order" (the URL path). This is the "Waiter" logic:

Questions​

  • What happens where there are multiple orders? In the restaurant analogy, you don't hire a new waiter for every single customer who walks in. Instead, one waiter (the Handler) is fast enough to take many orders at once, or you have a "hosting station" that directs traffic.

  • The Go standard library (net/http) is designed to be "concurrent by default." Every time a request hits your server, the ListenAndServe function automatically triggers a new goroutine for that specific request.

Here's the code snippet that handles:

// This code is ALREADY running in a goroutine automatically!
func (h *Handler) getProducts(w http.ResponseWriter, r *http.Request) {
// The Chef (Service) can handle multiple waiters asking for meat at once
products, err := h.service.GetProducts()
if err != nil {
http.Error(w, "Error", 500)
return
}
json.NewEncoder(w).Encode(products)
}