├── README.md ├── main.go └── setup.sql /README.md: -------------------------------------------------------------------------------- 1 | # Web App to API Demo 2 | 3 | This app is meant to help demonstrate how easy it can be to migrate a Go application that renders HTML into a JSON API, and how nearly all of the logic in the application will remain unchanged. It intentionally starts out with a pretty poor design and structure so that we can look at the benefits of each individual set of changes we will be making. 4 | 5 | ## Setup 6 | 7 | To setup your local dev you will need to setup a PostgreSQL database. I provided a `setup.sql` file to help make that a little easier - you should be able to run it like this: 8 | 9 | ``` 10 | psql -f setup.sql 11 | ``` 12 | 13 | If you need help figuring our Postgres, I have a pretty in-depth series on using it here: 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "html/template" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/gorilla/mux" 13 | _ "github.com/lib/pq" 14 | ) 15 | 16 | const ( 17 | host = "localhost" 18 | port = 5432 19 | user = "postgres" 20 | password = "your-password" 21 | dbname = "widget_demo" 22 | ) 23 | 24 | var ( 25 | db *sql.DB 26 | ) 27 | 28 | func main() { 29 | // setup the DB connection 30 | psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+ 31 | "password=%s dbname=%s sslmode=disable", 32 | host, port, user, password, dbname) 33 | var err error 34 | db, err = sql.Open("postgres", psqlInfo) 35 | if err != nil { 36 | panic(err) 37 | } 38 | defer db.Close() 39 | err = db.Ping() 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | r := mux.NewRouter() 45 | r.Handle("/", http.RedirectHandler("/signin", http.StatusFound)) 46 | r.HandleFunc("/signin", showSignin).Methods("GET") 47 | r.HandleFunc("/signin", processSignin).Methods("POST") 48 | r.HandleFunc("/widgets", allWidgets).Methods("GET") 49 | r.HandleFunc("/widgets", createWidget).Methods("POST") 50 | r.HandleFunc("/widgets/new", newWidget).Methods("GET") 51 | log.Fatal(http.ListenAndServe(":3000", r)) 52 | } 53 | 54 | func newWidget(w http.ResponseWriter, r *http.Request) { 55 | // Ignore auth for now - do it on the POST 56 | html := ` 57 | 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | ` 72 | fmt.Fprint(w, html) 73 | } 74 | 75 | func createWidget(w http.ResponseWriter, r *http.Request) { 76 | // Verify the user is signed in 77 | session, err := r.Cookie("session") 78 | if err != nil { 79 | http.Redirect(w, r, "/signin", http.StatusFound) 80 | return 81 | } 82 | row := db.QueryRow(`SELECT id FROM users WHERE token=$1;`, session.Value) 83 | var userID int 84 | err = row.Scan(&userID) 85 | if err != nil { 86 | switch err { 87 | case sql.ErrNoRows: 88 | // Email doesn't map to a user in our DB 89 | http.Redirect(w, r, "/signin", http.StatusFound) 90 | default: 91 | http.Error(w, "Something went wrong. Try again later.", http.StatusInternalServerError) 92 | } 93 | return 94 | } 95 | 96 | // Parse form values and validate data (pretend w/ me here) 97 | name := r.PostFormValue("name") 98 | color := r.PostFormValue("color") 99 | price, err := strconv.Atoi(r.PostFormValue("price")) 100 | if err != nil { 101 | http.Error(w, "Invalid price", http.StatusBadRequest) 102 | return 103 | } 104 | if color == "Green" && price%2 != 0 { 105 | http.Error(w, "Price must be even with a color of Green", http.StatusBadRequest) 106 | return 107 | } 108 | 109 | // Create a new widget! 110 | _, err = db.Exec(`INSERT INTO widgets(userID, name, price, color) VALUES($1, $2, $3, $4)`, userID, name, price, color) 111 | if err != nil { 112 | http.Error(w, "Something went wrong. Try again later.", http.StatusInternalServerError) 113 | return 114 | } 115 | http.Redirect(w, r, "/widgets", http.StatusFound) 116 | } 117 | 118 | func allWidgets(w http.ResponseWriter, r *http.Request) { 119 | // Verify the user is signed in 120 | session, err := r.Cookie("session") 121 | if err != nil { 122 | http.Redirect(w, r, "/signin", http.StatusFound) 123 | return 124 | } 125 | row := db.QueryRow(`SELECT id FROM users WHERE token=$1;`, session.Value) 126 | var userID int 127 | err = row.Scan(&userID) 128 | if err != nil { 129 | switch err { 130 | case sql.ErrNoRows: 131 | // Email doesn't map to a user in our DB 132 | http.Redirect(w, r, "/signin", http.StatusFound) 133 | default: 134 | http.Error(w, "Something went wrong. Try again later.", http.StatusInternalServerError) 135 | } 136 | return 137 | } 138 | 139 | // Query for this user's widgets 140 | rows, err := db.Query(`SELECT id, name, price, color FROM widgets WHERE userID=$1`, userID) 141 | if err != nil { 142 | http.Error(w, "Something went wrong.", http.StatusInternalServerError) 143 | return 144 | } 145 | defer rows.Close() 146 | var widgets []Widget 147 | for rows.Next() { 148 | var widget Widget 149 | err = rows.Scan(&widget.ID, &widget.Name, &widget.Price, &widget.Color) 150 | if err != nil { 151 | log.Printf("Failed to scan a widget: %v", err) 152 | continue 153 | } 154 | widgets = append(widgets, widget) 155 | } 156 | 157 | // Render the widgets 158 | tplStr := ` 159 | 160 | 161 |

Widgets

162 | 167 |

168 | Create a new widget 169 |

170 | ` 171 | tpl := template.Must(template.New("").Parse(tplStr)) 172 | err = tpl.Execute(w, widgets) 173 | if err != nil { 174 | http.Error(w, "Something went wrong.", http.StatusInternalServerError) 175 | return 176 | } 177 | } 178 | 179 | type Widget struct { 180 | ID int 181 | UserID int 182 | Name string 183 | Price int 184 | Color string 185 | } 186 | 187 | func showSignin(w http.ResponseWriter, r *http.Request) { 188 | html := ` 189 | 190 | 191 |
192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 |
200 | ` 201 | fmt.Fprint(w, html) 202 | } 203 | 204 | func processSignin(w http.ResponseWriter, r *http.Request) { 205 | email := r.PostFormValue("email") 206 | password := r.PostFormValue("password") 207 | // Fake the password part 208 | if password != "demo" { 209 | http.Redirect(w, r, "/signin", http.StatusNotFound) 210 | return 211 | } 212 | 213 | // Lookup the user ID by their email in the DB 214 | email = strings.ToLower(email) 215 | row := db.QueryRow(`SELECT id FROM users WHERE email=$1;`, email) 216 | var id int 217 | err := row.Scan(&id) 218 | if err != nil { 219 | switch err { 220 | case sql.ErrNoRows: 221 | // Email doesn't map to a user in our DB 222 | http.Redirect(w, r, "/signin", http.StatusFound) 223 | default: 224 | http.Error(w, "Something went wrong. Try again later.", http.StatusInternalServerError) 225 | } 226 | return 227 | } 228 | 229 | // Create a fake session token 230 | token := fmt.Sprintf("fake-session-id-%d", id) 231 | _, err = db.Exec(`UPDATE users SET token=$2 WHERE id=$1`, id, token) 232 | if err != nil { 233 | http.Error(w, "Something went wrong. Try again later.", http.StatusInternalServerError) 234 | return 235 | } 236 | cookie := http.Cookie{ 237 | Name: "session", 238 | Value: token, 239 | } 240 | http.SetCookie(w, &cookie) 241 | http.Redirect(w, r, "/widgets", http.StatusFound) 242 | } 243 | -------------------------------------------------------------------------------- /setup.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE widget_demo; 2 | 3 | \c widget_demo 4 | CREATE TABLE users ( 5 | id SERIAL PRIMARY KEY, 6 | email TEXT UNIQUE NOT NULL, 7 | token TEXT 8 | ); 9 | 10 | CREATE TABLE widgets ( 11 | id SERIAL PRIMARY KEY, 12 | userID INT NOT NULL, 13 | name TEXT NOT NULL, 14 | price INT NOT NULL, 15 | color TEXT NOT NULL 16 | ); 17 | 18 | -- To speed up setup 19 | INSERT INTO users (email) 20 | VALUES ('jon@calhoun.io'); 21 | 22 | INSERT INTO widgets (userID, name, price, color) 23 | VALUES (1, 'Go Widget', 12, 'Green'); 24 | 25 | INSERT INTO widgets (userID, name, price, color) 26 | VALUES (1, 'Slow Widget', 22, 'Yellow'); 27 | 28 | INSERT INTO widgets (userID, name, price, color) 29 | VALUES (1, 'Stop Widget', 18, 'Red'); 30 | --------------------------------------------------------------------------------