Merge changes Iaf5b2c03,I7f579129,I0572a214,Id496b032

* changes:
  scouting: Allow the requests handler access to the database
  Move scouting/db.go into its own directory
  Handle webserver requests from the scouting web page
  Create a library for serving static files
diff --git a/BUILD b/BUILD
index e7fa267..da7cb6e 100644
--- a/BUILD
+++ b/BUILD
@@ -12,6 +12,9 @@
 # gazelle:exclude third_party
 # gazelle:exclude external
 # gazelle:resolve go github.com/frc971/971-Robot-Code/build_tests/fbs //build_tests:test_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response //scouting/webserver/requests/messages:error_response_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting //scouting/webserver/requests/messages:submit_data_scouting_go_fbs
+# gazelle:resolve go github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response //scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs
 
 gazelle(
     name = "gazelle",
diff --git a/scouting/BUILD b/scouting/BUILD
index 8a3b3f0..836e5c3 100644
--- a/scouting/BUILD
+++ b/scouting/BUILD
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 
 go_binary(
     name = "sql_demo",
@@ -15,19 +15,3 @@
     visibility = ["//visibility:private"],
     deps = ["@com_github_mattn_go_sqlite3//:go-sqlite3"],
 )
-
-go_library(
-    name = "database",
-    srcs = ["db.go"],
-    importpath = "github.com/frc971/971-Robot-Code/scouting",
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-    visibility = ["//visibility:private"],
-    deps = ["@com_github_mattn_go_sqlite3//:go_default_library"],
-)
-
-go_test(
-    name = "db_test",
-    srcs = ["db_test.go"],
-    embed = [":database"],
-    target_compatible_with = ["@platforms//cpu:x86_64"],
-)
diff --git a/scouting/db/BUILD b/scouting/db/BUILD
new file mode 100644
index 0000000..e89332d
--- /dev/null
+++ b/scouting/db/BUILD
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "db",
+    srcs = ["db.go"],
+    importpath = "github.com/frc971/971-Robot-Code/scouting/db",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
+    deps = ["@com_github_mattn_go_sqlite3//:go-sqlite3"],
+)
+
+go_test(
+    name = "db_test",
+    srcs = ["db_test.go"],
+    embed = [":db"],
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+)
diff --git a/scouting/db.go b/scouting/db/db.go
similarity index 100%
rename from scouting/db.go
rename to scouting/db/db.go
diff --git a/scouting/db_test.go b/scouting/db/db_test.go
similarity index 100%
rename from scouting/db_test.go
rename to scouting/db/db_test.go
diff --git a/scouting/webserver/BUILD b/scouting/webserver/BUILD
new file mode 100644
index 0000000..66db8e8
--- /dev/null
+++ b/scouting/webserver/BUILD
@@ -0,0 +1,22 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "webserver_lib",
+    srcs = ["main.go"],
+    importpath = "github.com/frc971/971-Robot-Code/scouting/webserver",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:private"],
+    deps = [
+        "//scouting/db",
+        "//scouting/webserver/requests",
+        "//scouting/webserver/server",
+        "//scouting/webserver/static",
+    ],
+)
+
+go_binary(
+    name = "webserver",
+    embed = [":webserver_lib"],
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
+)
diff --git a/scouting/webserver/README.md b/scouting/webserver/README.md
new file mode 100644
index 0000000..1c2383b
--- /dev/null
+++ b/scouting/webserver/README.md
@@ -0,0 +1,26 @@
+The scouting web server
+================================================================================
+
+This directory contains the code that combines to make the scouting web server.
+
+`main.go`
+--------------------------------------------------------------------------------
+This is the main application that brings all the pieces together. Run it like
+so:
+```bash
+bazel run //scouting/webserver:webserver
+```
+
+`server/`
+--------------------------------------------------------------------------------
+This directory contains the code that manages the web server itself. It's
+responsible for starting and stopping the server. It also exposes a `Handle()`
+method that lets other libraries enhance the server's functionality.
+
+`static/`
+--------------------------------------------------------------------------------
+This directory contains the code that serves static files at the root of the
+server. Make sure that none of the files you're serving clash with any of the
+paths used by the other libraries that enhance the server's functionality. E.g.
+if POST requests for match data are serviced at `/requests/xyz`, then don't
+serve any files in a `requests` directory.
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
new file mode 100644
index 0000000..668104b
--- /dev/null
+++ b/scouting/webserver/main.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/frc971/971-Robot-Code/scouting/db"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/static"
+)
+
+func main() {
+	portPtr := flag.Int("port", 8080, "The port number to bind to.")
+	dirPtr := flag.String("directory", ".", "The directory to serve at /.")
+	flag.Parse()
+
+	database, err := db.NewDatabase()
+	if err != nil {
+		log.Fatal("Failed to connect to database: ", err)
+	}
+
+	scoutingServer := server.NewScoutingServer()
+	static.ServePages(scoutingServer, *dirPtr)
+	requests.HandleRequests(database, scoutingServer)
+	scoutingServer.Start(*portPtr)
+	fmt.Println("Serving", *dirPtr, "on port", *portPtr)
+
+	// Block until the user hits Ctrl-C.
+	sigint := make(chan os.Signal, 1)
+	signal.Notify(sigint, syscall.SIGINT)
+	fmt.Println("Waiting for CTRL-C or SIGINT.")
+	<-sigint
+
+	fmt.Println("Shutting down.")
+	scoutingServer.Stop()
+	fmt.Println("Successfully shut down.")
+}
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
new file mode 100644
index 0000000..15e4c05
--- /dev/null
+++ b/scouting/webserver/requests/BUILD
@@ -0,0 +1,32 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "requests",
+    srcs = ["requests.go"],
+    importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/requests",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//scouting/db",
+        "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
+        "//scouting/webserver/server",
+        "@com_github_google_flatbuffers//go:go_default_library",
+    ],
+)
+
+go_test(
+    name = "requests_test",
+    srcs = ["requests_test.go"],
+    embed = [":requests"],
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = [
+        "//scouting/db",
+        "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
+        "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
+        "//scouting/webserver/server",
+        "@com_github_google_flatbuffers//go:go_default_library",
+    ],
+)
diff --git a/scouting/webserver/requests/messages/BUILD b/scouting/webserver/requests/messages/BUILD
new file mode 100644
index 0000000..a32ca57
--- /dev/null
+++ b/scouting/webserver/requests/messages/BUILD
@@ -0,0 +1,21 @@
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_go_library", "flatbuffer_ts_library")
+
+[(
+    flatbuffer_go_library(
+        name = name + "_go_fbs",
+        srcs = [name + ".fbs"],
+        importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/" + name,
+        target_compatible_with = ["@platforms//cpu:x86_64"],
+        visibility = ["//visibility:public"],
+    ),
+    flatbuffer_ts_library(
+        name = name + "_ts_fbs",
+        srcs = [name + ".fbs"],
+        target_compatible_with = ["@platforms//cpu:x86_64"],
+        visibility = ["//visibility:public"],
+    ),
+) for name in (
+    "error_response",
+    "submit_data_scouting",
+    "submit_data_scouting_response",
+)]
diff --git a/scouting/webserver/requests/messages/error_response.fbs b/scouting/webserver/requests/messages/error_response.fbs
new file mode 100644
index 0000000..4371cb5
--- /dev/null
+++ b/scouting/webserver/requests/messages/error_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table ErrorResponse {
+    error_message:string (id: 0);
+}
+
+root_type ErrorResponse;
diff --git a/scouting/webserver/requests/messages/submit_data_scouting.fbs b/scouting/webserver/requests/messages/submit_data_scouting.fbs
new file mode 100644
index 0000000..63bba7a
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_data_scouting.fbs
@@ -0,0 +1,11 @@
+namespace scouting.webserver.requests;
+
+table SubmitDataScouting {
+    team:int (id: 0);
+    match:int (id: 1);
+
+    upper_goal_hits:int (id: 2);
+    // TODO: Implement the rest of this.
+}
+
+root_type SubmitDataScouting;
diff --git a/scouting/webserver/requests/messages/submit_data_scouting_response.fbs b/scouting/webserver/requests/messages/submit_data_scouting_response.fbs
new file mode 100644
index 0000000..fb7f765
--- /dev/null
+++ b/scouting/webserver/requests/messages/submit_data_scouting_response.fbs
@@ -0,0 +1,7 @@
+namespace scouting.webserver.requests;
+
+table SubmitDataScoutingResponse {
+    // TODO: Implement this.
+}
+
+root_type SubmitDataScoutingResponse;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
new file mode 100644
index 0000000..2462ccb
--- /dev/null
+++ b/scouting/webserver/requests/requests.go
@@ -0,0 +1,85 @@
+package requests
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/frc971/971-Robot-Code/scouting/db"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
+	_ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
+	flatbuffers "github.com/google/flatbuffers/go"
+)
+
+// The interface we expect the database abstraction to conform to.
+// We use an interface here because it makes unit testing easier.
+type Database interface {
+	AddToMatch(db.Match) error
+	AddToStats(db.Stats) error
+	ReturnMatches() ([]db.Match, error)
+	ReturnStats() ([]db.Stats, error)
+	QueryMatches(int) ([]db.Match, error)
+	QueryStats(int) ([]db.Stats, error)
+}
+
+// Handles unknown requests. Just returns a 404.
+func unknown(w http.ResponseWriter, req *http.Request) {
+	w.WriteHeader(http.StatusNotFound)
+}
+
+func respondWithError(w http.ResponseWriter, statusCode int, errorMessage string) {
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&error_response.ErrorResponseT{
+		ErrorMessage: errorMessage,
+	}).Pack(builder))
+	w.WriteHeader(statusCode)
+	w.Write(builder.FinishedBytes())
+}
+
+func respondNotImplemented(w http.ResponseWriter) {
+	respondWithError(w, http.StatusNotImplemented, "")
+}
+
+// TODO(phil): Can we turn this into a generic?
+func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*submit_data_scouting.SubmitDataScouting, bool) {
+	success := true
+	defer func() {
+		if r := recover(); r != nil {
+			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
+			success = false
+		}
+	}()
+	result := submit_data_scouting.GetRootAsSubmitDataScouting(buf, 0)
+	return result, success
+}
+
+// Handles a SubmitDataScouting request.
+type submitDataScoutingHandler struct {
+	db Database
+}
+
+func (handler submitDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	_, success := parseSubmitDataScouting(w, requestBytes)
+	if !success {
+		return
+	}
+
+	// TODO(phil): Actually handle the request.
+	// We have access to the database via "handler.db" here. For example:
+	// stats := handler.db.ReturnStats()
+
+	respondNotImplemented(w)
+}
+
+func HandleRequests(db Database, scoutingServer server.ScoutingServer) {
+	scoutingServer.HandleFunc("/requests", unknown)
+	scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
+}
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
new file mode 100644
index 0000000..6f62ce3
--- /dev/null
+++ b/scouting/webserver/requests/requests_test.go
@@ -0,0 +1,114 @@
+package requests
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"testing"
+
+	"github.com/frc971/971-Robot-Code/scouting/db"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
+	_ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
+	flatbuffers "github.com/google/flatbuffers/go"
+)
+
+// Validates that an unhandled address results in a 404.
+func Test404(t *testing.T) {
+	db := MockDatabase{}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&db, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	resp, err := http.Get("http://localhost:8080/requests/foo")
+	if err != nil {
+		t.Fatalf("Failed to get data: %v", err)
+	}
+	if resp.StatusCode != http.StatusNotFound {
+		t.Fatalf("Expected error code 404, but got %d instead", resp.Status)
+	}
+}
+
+// Validates that we can submit new data scouting data.
+func TestSubmitDataScoutingError(t *testing.T) {
+	db := MockDatabase{}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&db, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	resp, err := http.Post("http://localhost:8080/requests/submit/data_scouting", "application/octet-stream", bytes.NewReader([]byte("")))
+	if err != nil {
+		t.Fatalf("Failed to send request: %v", err)
+	}
+	if resp.StatusCode != http.StatusBadRequest {
+		t.Fatal("Unexpected status code. Got", resp.Status)
+	}
+
+	responseBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatal("Failed to read response bytes:", err)
+	}
+	errorResponse := error_response.GetRootAsErrorResponse(responseBytes, 0)
+
+	errorMessage := string(errorResponse.ErrorMessage())
+	if errorMessage != "Failed to parse SubmitDataScouting: runtime error: index out of range [3] with length 0" {
+		t.Fatal("Got mismatched error message:", errorMessage)
+	}
+}
+
+// Validates that we can submit new data scouting data.
+func TestSubmitDataScouting(t *testing.T) {
+	db := MockDatabase{}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&db, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&submit_data_scouting.SubmitDataScoutingT{
+		Team:          971,
+		Match:         1,
+		UpperGoalHits: 9971,
+	}).Pack(builder))
+
+	resp, err := http.Post("http://localhost:8080/requests/submit/data_scouting", "application/octet-stream", bytes.NewReader(builder.FinishedBytes()))
+	if err != nil {
+		t.Fatalf("Failed to send request: %v", err)
+	}
+	if resp.StatusCode != http.StatusNotImplemented {
+		t.Fatal("Unexpected status code. Got", resp.Status)
+	}
+	// TODO(phil): We have nothing to validate yet. Fix that.
+}
+
+// A mocked database we can use for testing. Add functionality to this as
+// needed for your tests.
+
+type MockDatabase struct{}
+
+func (database *MockDatabase) AddToMatch(db.Match) error {
+	return nil
+}
+
+func (database *MockDatabase) AddToStats(db.Stats) error {
+	return nil
+}
+
+func (database *MockDatabase) ReturnMatches() ([]db.Match, error) {
+	return []db.Match{}, nil
+}
+
+func (database *MockDatabase) ReturnStats() ([]db.Stats, error) {
+	return []db.Stats{}, nil
+}
+
+func (database *MockDatabase) QueryMatches(int) ([]db.Match, error) {
+	return []db.Match{}, nil
+}
+
+func (database *MockDatabase) QueryStats(int) ([]db.Stats, error) {
+	return []db.Stats{}, nil
+}
diff --git a/scouting/webserver/server/BUILD b/scouting/webserver/server/BUILD
new file mode 100644
index 0000000..7ac56db
--- /dev/null
+++ b/scouting/webserver/server/BUILD
@@ -0,0 +1,9 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "server",
+    srcs = ["server.go"],
+    importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/server",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
+)
diff --git a/scouting/webserver/server/server.go b/scouting/webserver/server/server.go
new file mode 100644
index 0000000..c21e2a1
--- /dev/null
+++ b/scouting/webserver/server/server.go
@@ -0,0 +1,114 @@
+// This file implements a web server that can be used by the main scouting
+// application. It can also be used in unit tests for individual components of
+// the web server.
+
+package server
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"time"
+)
+
+type ScoutingServer interface {
+	// Add a handler for a particular path. See upstream docs for this:
+	// https://pkg.go.dev/net/http#ServeMux.Handle
+	Handle(string, http.Handler)
+	// Add a handler function for a particular path. See upstream docs:
+	// https://pkg.go.dev/net/http#ServeMux.HandleFunc
+	HandleFunc(string, func(http.ResponseWriter, *http.Request))
+	// Starts the server on the specified port. Handlers cannot be added
+	// once this function is called.
+	Start(int)
+	// Stops the server.
+	Stop()
+}
+
+// This is a collection of data we'll need to implement the ScoutingServer
+// interface.
+type scoutingServer struct {
+	mux        *http.ServeMux
+	httpServer *http.Server
+	doneChan   <-chan bool
+	// Denotes whether the server has ever been Start()ed before.
+	started bool
+	// Denotes whether or not the server is currently running.
+	running bool
+}
+
+// Instantiates a new ScoutingServer.
+func NewScoutingServer() ScoutingServer {
+	return &scoutingServer{
+		mux:        http.NewServeMux(),
+		httpServer: nil,
+		doneChan:   nil,
+		started:    false,
+		running:    false,
+	}
+}
+
+func (server *scoutingServer) Handle(path string, handler http.Handler) {
+	if server.started {
+		log.Fatal("Cannot add handlers once server has started.")
+	}
+	server.mux.Handle(path, handler)
+}
+
+func (server *scoutingServer) HandleFunc(path string, handler func(http.ResponseWriter, *http.Request)) {
+	if server.started {
+		log.Fatal("Cannot add handlers once server has started.")
+	}
+	server.mux.HandleFunc(path, handler)
+}
+
+func (server *scoutingServer) Start(port int) {
+	if server.started {
+		log.Fatal("Cannot Start() a server a second time.")
+	}
+	server.started = true
+	server.running = true
+
+	addressStr := fmt.Sprintf(":%d", port)
+	server.httpServer = &http.Server{
+		Addr:    addressStr,
+		Handler: server.mux,
+	}
+
+	doneChan := make(chan bool, 1)
+	server.doneChan = doneChan
+
+	// Start the server in the background since the ListenAndServe() call
+	// blocks.
+	go func() {
+		if err := server.httpServer.ListenAndServe(); err != http.ErrServerClosed {
+			log.Fatalf("Error calling ListenAndServe(): %v", err)
+		}
+		close(doneChan)
+	}()
+
+	// Wait until the server is ready.
+	for {
+		dial, err := net.Dial("tcp", addressStr)
+		if err != nil {
+			time.Sleep(100 * time.Millisecond)
+		} else {
+			dial.Close()
+			break
+		}
+	}
+}
+
+func (server *scoutingServer) Stop() {
+	if !server.running {
+		log.Fatal("Cannot Stop() a server that's not running.")
+	}
+	server.running = false
+
+	if err := server.httpServer.Shutdown(context.Background()); err != nil {
+		log.Fatalf("Error shutting down the server: %v", err)
+	}
+	<-server.doneChan
+}
diff --git a/scouting/webserver/static/BUILD b/scouting/webserver/static/BUILD
new file mode 100644
index 0000000..3166853
--- /dev/null
+++ b/scouting/webserver/static/BUILD
@@ -0,0 +1,22 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "static",
+    srcs = ["static.go"],
+    importpath = "github.com/frc971/971-Robot-Code/scouting/webserver/static",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    visibility = ["//visibility:public"],
+    deps = ["//scouting/webserver/server"],
+)
+
+go_test(
+    name = "static_test",
+    srcs = ["static_test.go"],
+    data = [
+        "test_pages/page.txt",
+        "test_pages/root.txt",
+    ],
+    embed = [":static"],
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = ["//scouting/webserver/server"],
+)
diff --git a/scouting/webserver/static/static.go b/scouting/webserver/static/static.go
new file mode 100644
index 0000000..e921b0b
--- /dev/null
+++ b/scouting/webserver/static/static.go
@@ -0,0 +1,14 @@
+package static
+
+// A year agnostic way to serve static http files.
+import (
+	"net/http"
+
+	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
+)
+
+// Serve pages given a port, directory to serve from, and an channel to pass the errors back to the caller.
+func ServePages(scoutingServer server.ScoutingServer, directory string) {
+	// Serve the / endpoint given a folder of pages.
+	scoutingServer.Handle("/", http.FileServer(http.Dir(directory)))
+}
diff --git a/scouting/webserver/static/static_test.go b/scouting/webserver/static/static_test.go
new file mode 100644
index 0000000..15bd872
--- /dev/null
+++ b/scouting/webserver/static/static_test.go
@@ -0,0 +1,57 @@
+package static
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"testing"
+
+	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
+)
+
+func TestServing(t *testing.T) {
+	cases := []struct {
+		// The path to request from the server.
+		path string
+		// The data that the server is expected to return at the
+		// specified path.
+		expectedData string
+	}{
+		{"/root.txt", "Hello, this is the root page!"},
+		{"/page.txt", "Hello from a page!"},
+	}
+
+	scoutingServer := server.NewScoutingServer()
+	ServePages(scoutingServer, "test_pages")
+	scoutingServer.Start(8080)
+
+	// Go through all the test cases, and run them against the running webserver.
+	for _, c := range cases {
+		dataReceived := getData(c.path, t)
+		if dataReceived != c.expectedData {
+			t.Errorf("Got %q, but expected %q", dataReceived, c.expectedData)
+		}
+	}
+
+	scoutingServer.Stop()
+}
+
+// Retrieves the data at the specified path. If an error occurs, the test case
+// is terminated and failed.
+func getData(path string, t *testing.T) string {
+	resp, err := http.Get(fmt.Sprintf("http://localhost:8080/%s", path))
+	if err != nil {
+		t.Fatalf("Failed to get data ", err)
+	}
+	// Error out if the return status is anything other than 200 OK.
+	if resp.Status != "200 OK" {
+		t.Fatalf("Received a status code other than 200")
+	}
+	// Read the response body.
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatalf("Failed to read body")
+	}
+	// Decode the body and return it.
+	return string(body)
+}
diff --git a/scouting/webserver/static/test_pages/page.txt b/scouting/webserver/static/test_pages/page.txt
new file mode 100644
index 0000000..3bb47e8
--- /dev/null
+++ b/scouting/webserver/static/test_pages/page.txt
@@ -0,0 +1 @@
+Hello from a page!
\ No newline at end of file
diff --git a/scouting/webserver/static/test_pages/root.txt b/scouting/webserver/static/test_pages/root.txt
new file mode 100644
index 0000000..8f75b7d
--- /dev/null
+++ b/scouting/webserver/static/test_pages/root.txt
@@ -0,0 +1 @@
+Hello, this is the root page!
\ No newline at end of file
diff --git a/third_party/flatbuffers/build_defs.bzl b/third_party/flatbuffers/build_defs.bzl
index c547a7d..98cc897 100644
--- a/third_party/flatbuffers/build_defs.bzl
+++ b/third_party/flatbuffers/build_defs.bzl
@@ -34,6 +34,7 @@
 
 DEFAULT_FLATC_GO_ARGS = [
     "--gen-onefile",
+    "--gen-object-api",
 ]
 
 DEFAULT_FLATC_TS_ARGS = [