Getting started with Go and React - React & REST API's
Creating an REST API backend in Go and connecting it to a React.js frontend.
- Part I - Webservice, Routing and Status Log
- Part II - API Routes
- Part III - PostgreSQL
- Part III - React & REST API's
I want to prototype a Go backend for a Weather Cam tool. The backend should hold all the information related to all cameras and serve them on different routes. The backend then needs to be connected to a React.js frontend that displays the JSON data that is being served as well as to allow to add / delete cameras.
Go CORS Middleware
Access to fetch at 'http://localhost:4000/v1/cameras' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
To be able to connect our React frontend with the Go backend, we first need to take care of our CORS header. Since the applications are running on different ports they will be interpreted as different webpages by our browsers - and the API will be blocked by default. We need a middleware that adds a allow all origins header to every request:
./src/api/middleware.go
package main
import "net/http"
func (app *application) enableCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r)
})
}
The middleware just needs to be wrapped around our routes:
//BEFORE
func (app *application) routes() *httprouter.Router {
...
return router
}
//AFTER
func (app *application) routes() http.Handler {
...
return app.enableCORS(router)
}
React Frontend
Display Camera List
So far we hardcoded a list of cameras to our React component in:
src/components/cameras.jsx
state= {cameras: []}
componentDidMount() {
this.setState({
cameras: [
{id: 1, name: "A Camera", location: "Somewhere"},
{id: 2, name: "B Camera", location: "Nowhere"},
{id: 3, name: "C Camera", location: "Knowhere"}
]
})
}
Here we now need to add our API request list:
state= {
cameras: [],
isLoaded: false,
}
componentDidMount() {
fetch("http://localhost:4000/v1/cameras")
.then((response) => response.json())
.then((json) => {
this.setState(
cameras: json.cameras,
isLoaded: true
)
})
}
To render the response:
render() {
const { cameras, isLoaded } = this.state;
if (!isLoaded) {
return <p>Loading ...</p>
} else {
return (
<>
<h2>Cameras</h2>
<ul>
{cameras.map( (m) => (
<li key={m.id}>
<Link to={`/cameras/${m.id}`}>{m.name}</Link>
</li>
))}
</ul>
</>
)
}}
Go and visit the frontend http://localhost:3000/#/cameras
- it should now render the list of cameras defined in our Postgres database!
Error Handling
So far we have a variable called isLoaded
that is set to true once we get the JSON response from our API. We are using an IF statement to replace a "Loading..." paragraph with the actual data once this happens. But we still need to handle the case that our API returns something that is not the correct response:
./src/components/cameras.jsx
export default class Cameras extends Component {
state = {
cameras: [],
isLoaded: false,
// Add state for error handling
error: null,
}
componentDidMount() {
fetch('http://localhost:4000/v1/cameras')
// .then((response) => response.json())
// Replace above with error handler
.then((response) => {
// Debugging: Print status code to console
// console.log("API Response Status Code: ", response.status)
if (response.status !== '200') {
let err = Error
err.message = 'Invalid API Response Code: ' + response.status
this.setState({ error: err })
}
return response.json()
})
.then((json) => {
this.setState(
{
cameras: json.cameras,
isLoaded: true,
},
// If status code is not 200 export error instead
(error) => {
this.setState({
isLoaded: true,
error,
})
}
)
})
}
render() {
const { cameras, isLoaded, error } = this.state
// Print error if error is not null
if (error) {
return <div>Error: {error.message}</div>
// Or print `Loading...` until JSON response
} else if (!isLoaded) {
return <p>Loading ...</p>
// Once it is loaded print response
} else {
return (
<>
<h2>Cameras</h2>
<ul>
{cameras.map((m) => (
<li key={m.id}>
<Link to={`/cameras/${m.id}`}>{m.name}</Link>
</li>
))}
</ul>
</>
)
}
}
}
Display Single Camera Details
./src/components/camera.jsx
For the camera detail page we can recycle most of this code:
export default class Camera extends Component {
state = {
camera: [],
isLoaded: false,
error: null,
}
componentDidMount() {
fetch('http://localhost:4000/v1/camera/' + this.props.match.params.id)
// .then((response) => response.json())
// Replace above with error handler
.then((response) => {
// Debugging: Print status code to console
// console.log("API Response Status Code: ", response.status)
if (response.status !== '200') {
let err = Error
err.message = 'Invalid API Response Code: ' + response.status
this.setState({ error: err })
}
return response.json()
})
.then((json) => {
this.setState(
{
camera: json.camera,
isLoaded: true,
},
// If status code is not 200 export error instead
(error) => {
this.setState({
isLoaded: true,
error,
})
}
)
})
}
render() {
const { camera, isLoaded, error } = this.state
// Print error if error is not null
if (error) {
return <div>Error: {error.message}</div>
// Or print `Loading...` until JSON response
} else if (!isLoaded) {
return <p>Loading ...</p>
// Once it is loaded print response
} else {
return (
<>
<h2>Camera Details</h2>
<table className="table table-compact table-striped mt-3">
<thead>
<tr>
<td>
<strong>Name|Rating:</strong>
</td>
<td>
{camera.name} |{' '}
<span className="badge bg-primary">{camera.rating}</span>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<strong>ID|CID|LID:</strong>
</td>
<td>
{camera.id} | {camera.location[0].cid} |{' '}
{camera.location[0].lid}{' '}
</td>
</tr>
<tr>
<td>
<strong>Location:</strong>
</td>
<td>{camera.location[0].location}</td>
</tr>
<tr>
<td>
<strong>Model|Lense|Resolution:</strong>
</td>
<td>
{camera.location[0].model} | {camera.location[0].lense} |{' '}
{camera.location[0].res}
</td>
</tr>
<tr>
<td>
<strong>Address:</strong>
</td>
<td>{camera.ip}</td>
</tr>
<tr>
<td>
<strong>Login:</strong>
</td>
<td>
{camera.usr} | {camera.pass}
</td>
</tr>
<tr>
<td>
<strong>Installed|Inspected:</strong>
</td>
<td>
{camera.installed} <br /> {camera.inspected}
</td>
</tr>
</tbody>
</table>
</>
)
}
}
}
Display all Camera Locations
Backend
Displaying camera locations is pretty much the same as displaying all cameras:
./src/components/locations.jsx
export default class Locations extends Component {
state = {
locations: [],
isLoaded: false,
error: null,
}
componentDidMount() {
fetch('http://localhost:4000/v1/locations/')
.then((response) => {
// Debugging: Print status code to console
// console.log("API Response Status Code: ", response.status)
if (response.status !== 200) {
let err = Error
err.message = 'Invalid API Response Code: ' + response.status
this.setState({ error: err })
}
return response.json()
})
.then((json) => {
// Debugging: Print locations to console
// console.log("Locations JSON: ", json.locations)
this.setState(
{
locations: json.locations,
isLoaded: true,
},
// Debugging: Print locations State to console
// () => {console.log("Locations State: ", this.state.locations)},
// If status code is not 200 export error instead
(error) => {
this.setState({
isLoaded: true,
error,
})
}
)
})
}
render() {
const { locations, isLoaded, error } = this.state
// Print error if error is not null
if (error) {
return <div>Error: {error.message}</div>
// Or print `Loading...` until JSON response
} else if (!isLoaded) {
return <p>Loading ...</p>
// Once it is loaded print response
} else {
return (
<>
<h2>Locations</h2>
<ul>
{locations.map((m) => (
<li key={m.id}>
<Link to={`/location/${m.id}`}>{m.location}</Link>
</li>
))}
</ul>
</>
)
}
}
}
But we now have to add the backend to handle the location URL. The SQL queries are defined in:
./go_backend/models/gocamDB.go
Here we need to add the following SQL query:
SELECT id, location, cid, lid, created, updated FROM camera_locations ORDER BY lid;
id | location | cid | lid | created | updated
----+---------------+-------------+---------+----------------------------+----------------------------
1 | Mountain View | INSTAR-0001 | HK-0001 | 2021-09-19 03:03:19.534528 | 2021-10-21 03:03:19.534528
2 | Harbour East | INSTAR-0002 | HK-0001 | 2021-09-28 03:03:19.534528 | 2021-10-21 03:03:19.534528
3 | Harbour West | INSTAR-0003 | HK-0001 | 2021-09-08 03:03:19.534528 | 2021-10-21 03:03:19.534528
4 | Beachfront | INSTAR-0001 | HK-0002 | 2021-10-09 03:03:19.534528 | 2021-10-21 03:03:19.534528
5 | Downtown | INSTAR-0005 | HK-0003 | 2021-08-28 03:03:19.534528 | 2021-10-21 03:03:19.534528
6 | Central Park | INSTAR-0001 | HK-0003 | 2021-03-22 03:03:19.534528 | 2021-10-21 03:03:19.534528
7 | Terminal | INSTAR-0002 | HK-0003 | 2021-09-18 03:03:19.534528 | 2021-10-21 03:03:19.534528
8 | Skyline | INSTAR-0003 | HK-0003 | 2021-08-17 03:03:19.534528 | 2021-10-21 03:03:19.534528
9 | Plaza | INSTAR-0004 | HK-0003 | 2021-09-06 03:03:19.534528 | 2021-10-21 03:03:19.534528
func (m *DBModel) LocationsAll() ( []*Location, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `SELECT id, location, cid, lid, created, updated FROM camera_locations ORDER BY lid`
rows, err := m.DB.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var locations []*Location
for rows.Next() {
var l Location
err := rows.Scan(
&l.ID,
&l.LocationName,
&l.CameraID,
&l.LocationID,
&l.Created,
&l.Updated,
)
if err != nil {
return nil, err
}
locations = append(locations, &l)
}
return locations, nil
}
To be able to use this model we now have to add a handler that takes care of the SQL request when the URL is called by our React App:
go_backend/src/api/cameraHandler.go
func (app *application) getAllLocations(w http.ResponseWriter, r *http.Request) {
locations, err := app.models.DB.LocationsAll()
if err != nil {
app.errorJSON(w, err)
return
}
err = app.writeJSON(w, http.StatusOK, locations, "locations")
if err != nil {
app.errorJSON(w, err)
return
}
}
Now we can provide a route that uses the handler:
go_backend/src/api/routes.go
func (app *application) routes() http.Handler {
router := httprouter.New()
// Route for status check handled by statusHandler
router.HandlerFunc(http.MethodGet, "/status", app.statusHandler)
// Routes for camera API check handled by cameraHandler
router.HandlerFunc(http.MethodGet, "/v1/camera/:id", app.getOneCamera)
router.HandlerFunc(http.MethodGet, "/v1/cameras", app.getAllCameras)
router.HandlerFunc(http.MethodGet, "/v1/locations", app.getAllLocations)
return app.enableCORS(router)
}
Frontend
export default class Cameras extends Component {
state = {
cameras: [],
isLoaded: false,
// Add state for error handling
error: null,
}
componentDidMount() {
fetch('http://localhost:4000/v1/cameras')
// .then((response) => response.json())
// Replace above with error handler
.then((response) => {
// Debugging: Print status code to console
// console.log("API Response Status Code: ", response.status)
if (response.status !== '200') {
let err = Error
err.message = 'Invalid API Response Code: ' + response.status
this.setState({ error: err })
}
return response.json()
})
.then((json) => {
this.setState(
{
cameras: json.cameras,
isLoaded: true,
},
// If status code is not 200 export error instead
(error) => {
this.setState({
isLoaded: true,
error,
})
}
)
})
}
render() {
const { cameras, isLoaded, error } = this.state
// Print error if error is not null
if (error) {
return <div>Error: {error.message}</div>
// Or print `Loading...` until JSON response
} else if (!isLoaded) {
return <p>Loading ...</p>
// Once it is loaded print response
} else {
return (
<>
<h2>Cameras</h2>
<ul>
{cameras.map((m) => (
<li key={m.id}>
<Link to={`/cameras/${m.id}`}>{m.name}</Link>
</li>
))}
</ul>
</>
)
}
}
}
Display all Cameras from a Location
Backend
Now that we have a list of locations with installed cameras we now need to add a component that displays all cameras from a selected location. Our backend already has a function that returns all cameras. We can modify this function to filter the results by the field location:
go_backend/models/gocamDB.go
He we are using the following SQL query to get all cameras:
SELECT id, name, usr, pass, ip, updated, created, rating FROM camera ORDER BY created DESC;
id | name | usr | pass | ip | updated | created | rating
----+---------------+-------+--------+-----------------+----------------------------+----------------------------+--------
9 | Plaza | admin | instar | 192.168.178.249 | 2021-10-22 03:32:09.828103 | 2021-10-15 03:32:09.828103 | 1
4 | Beachfront | admin | instar | 192.168.2.117 | 2021-10-22 03:32:09.828103 | 2021-09-20 03:32:09.828103 | 2
1 | Mountain View | admin | instar | 192.168.2.10 | 2021-10-22 03:32:09.828103 | 2021-06-21 03:32:09.828103 | 3
2 | Harbour East | admin | instar | 192.168.2.19 | 2021-10-22 03:32:09.828103 | 2021-06-01 03:32:09.828103 | 5
3 | Harbour West | admin | instar | 192.168.2.24 | 2021-10-22 03:32:09.828103 | 2021-05-19 03:32:09.828103 | 3
7 | Terminal | admin | instar | 192.168.178.52 | 2021-10-22 03:32:09.828103 | 2021-03-23 03:32:09.828103 | 2
8 | Skyline | admin | instar | 192.168.178.67 | 2021-10-22 03:32:09.828103 | 2021-03-02 03:32:09.828103 | 5
6 | Central Park | admin | instar | 192.168.178.42 | 2021-10-22 03:32:09.828103 | 2020-08-13 03:32:09.828103 | 2
5 | Downtown | admin | instar | 192.168.178.70 | 2021-10-22 03:32:09.828103 | 2020-07-12 03:32:09.828103 | 4
We need combine this with a query over the camera_location
table and filter by location ID:
SELECT camera_locations.id, camera_locations.location, camera_locations.cid, camera_locations.lid FROM camera_locations;
id | location | cid | lid
----+---------------+-------------+---------
1 | Mountain View | INSTAR-0001 | HK-0001
2 | Harbour East | INSTAR-0002 | HK-0001
3 | Harbour West | INSTAR-0003 | HK-0001
4 | Beachfront | INSTAR-0001 | HK-0002
5 | Downtown | INSTAR-0005 | HK-0003
6 | Central Park | INSTAR-0001 | HK-0003
7 | Terminal | INSTAR-0002 | HK-0003
8 | Skyline | INSTAR-0003 | HK-0003
9 | Plaza | INSTAR-0004 | HK-0003
E.g. filter for location ID HK-0001
:
SELECT id, name, usr, pass, ip, updated, created, rating FROM camera WHERE id IN (SELECT camera_locations.id FROM camera_locations WHERE camera_locations.lid = 'HK-0001' ) ORDER BY created DESC;
In our DBModel this looks like this:
// Return all cameras
// EDIT: add location ID as variable filter to All()
func(m *DBModel) All(locationID ...string) ([]*Camera, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Create variable to be able to filter by given locationID if provided
where := ""
// If locationID is provided set variable to filter query
if len(locationID) > 0 {
where = fmt.Sprintf("WHERE id IN (SELECT camera_locations.id FROM camera_locations WHERE camera_locations.lid = '%s')", locationID[0])
}
// SQL query to get all cameras
// EDIT: if where is defined filter by given lid
query := fmt.Sprintf(`SELECT id, name, usr, pass, ip, updated, created, rating FROM camera %s ORDER BY created DESC`, where)
...
This now handles both cases - where a location is provided it will filter by it and without a location it will insert an empty string and return cameras of all locations (just like before). All that is needed now is another function handler for the filter case:
./go_backend/src/api/cameraHandler.go
func (app *application) getCamerasByLocation(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
locationID := params.ByName("lid")
cameras, err := app.models.DB.All(locationID)
if err != nil {
app.errorJSON(w, err)
return
}
err = app.writeJSON(w, http.StatusOK, cameras, "cameras")
if err != nil {
app.errorJSON(w, err)
return
}
}
And this handler has to be added to it's route:
go_backend/src/api/routes.go
func (app *application) routes() http.Handler {
router := httprouter.New()
// Route for status check handled by statusHandler
router.HandlerFunc(http.MethodGet, "/status", app.statusHandler)
// Routes for camera API check handled by cameraHandler
router.HandlerFunc(http.MethodGet, "/v1/camera/:id", app.getOneCamera)
router.HandlerFunc(http.MethodGet, "/v1/cameras", app.getAllCameras)
router.HandlerFunc(http.MethodGet, "/v1/cameras/:lid", app.getCamerasByLocation)
router.HandlerFunc(http.MethodGet, "/v1/locations", app.getAllLocations)
return app.enableCORS(router)
}
Frontend
./src/components/location.jsx
export default class CameraLocation extends Component {
state = {
cameras: [],
isLoaded: false,
error: null,
}
componentDidMount() {
fetch('http://localhost:4000/v1/cameras/' + this.props.match.params.lid)
// .then((response) => response.json())
// Replace above with error handler
.then((response) => {
// Debugging: Print status code to console
// console.log("API Response Status Code: ", response.status)
if (response.status !== '200') {
let err = Error
err.message = 'Invalid API Response Code: ' + response.status
this.setState({ error: err })
}
return response.json()
})
.then((json) => {
this.setState(
{
cameras: json.cameras,
isLoaded: true,
},
// If status code is not 200 export error instead
(error) => {
this.setState({
isLoaded: true,
error,
})
}
)
})
}
render() {
let { cameras, isLoaded, error } = this.state
// If given location has no cameras set variable to empty array
if (!cameras) {
cameras = []
}
// Print error if error is not null
if (error) {
return <div>Error: {error.message}</div>
// Or print `Loading...` until JSON response
} else if (!isLoaded) {
return <p>Loading ...</p>
// Once it is loaded print response
} else {
return (
<>
<h2>
Location ID:{' '}
{this.props.location.pathname.split('/').pop().split(';')[0]}
</h2>
<ul>
{cameras.map((m) => (
<li key={m.id}>
<Link to={`/cameras/${m.id}`}>{m.name}</Link>
</li>
))}
</ul>
</>
)
}
}
}
Adding a route for the component:
./src/components/content.jsx
import CameraList from './cameras'
import Camera from './camera'
import LocationList from './locations'
import Location from './location'
...
<Switch>
<Route path="/cameras/:id" component={Camera} />
<Route path="/locations/:lid" component={Location} />
<Route exact path="/cameras">
<CameraList />
</Route>
<Route exact path="/locations">
<LocationList />
</Route>
...
</Switch>