├── 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 |
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 |
163 | {{range .}}
164 | - {{.Name}} - {{.Color}}: ${{.Price}}
165 | {{end}}
166 |
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 |
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 |
--------------------------------------------------------------------------------