Skip to main content

Getting started with Go and React - React & REST API's

Shenzhen, China

Creating an REST API backend in Go and connecting it to a React.js frontend.

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>