From 830a8805c9e06920da0f021a77a66bd25bae94d5 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Wed, 15 Oct 2014 13:01:11 -0800
Subject: [PATCH 1/9] Species Read - Order of ops:

router/routes.go
router/api.go
models/species_test.go
models/species.go
models/client.go
datastore/migrations/addspecies.sql
datastore/migrations/dropspecies.sql
datastore/species_test.go
datastore/species.go
datastore/datastore.go
api/species_test.go
api/species.go
api/handler.go
---
 api/handler.go                                |  2 +
 api/species.go                                | 22 ++++++
 api/species_test.go                           | 34 +++++++++
 datastore/datastore.go                        | 13 ++--
 .../migrations/00003_AddSpecies_down.sql      |  5 ++
 datastore/migrations/00003_AddSpecies_up.sql  | 15 ++++
 datastore/species.go                          | 22 ++++++
 datastore/species_test.go                     | 37 ++++++++++
 models/client.go                              |  6 +-
 models/species.go                             | 69 +++++++++++++++++++
 models/species_test.go                        | 39 +++++++++++
 router/api.go                                 |  4 ++
 router/routes.go                              |  2 +
 13 files changed, 263 insertions(+), 7 deletions(-)
 create mode 100644 api/species.go
 create mode 100644 api/species_test.go
 create mode 100644 datastore/migrations/00003_AddSpecies_down.sql
 create mode 100644 datastore/migrations/00003_AddSpecies_up.sql
 create mode 100644 datastore/species.go
 create mode 100644 datastore/species_test.go
 create mode 100644 models/species.go
 create mode 100644 models/species_test.go

diff --git a/api/handler.go b/api/handler.go
index a01506b..384740a 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -29,6 +29,8 @@ func Handler() *mux.Router {
 	m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus))
 	m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus))
 
+	m.Get(router.Species).Handler(handler(serveSpecies))
+
 	return m
 }
 
diff --git a/api/species.go b/api/species.go
new file mode 100644
index 0000000..c8e127c
--- /dev/null
+++ b/api/species.go
@@ -0,0 +1,22 @@
+package api
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/gorilla/mux"
+)
+
+func serveSpecies(w http.ResponseWriter, r *http.Request) error {
+	id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
+	if err != nil {
+		return err
+	}
+
+	species, err := store.Species.Get(id)
+	if err != nil {
+		return err
+	}
+
+	return writeJSON(w, species)
+}
diff --git a/api/species_test.go b/api/species_test.go
new file mode 100644
index 0000000..5d1bf87
--- /dev/null
+++ b/api/species_test.go
@@ -0,0 +1,34 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/thermokarst/bactdb/models"
+)
+
+func TestSpecies_Get(t *testing.T) {
+	setup()
+
+	want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	calledGet := false
+	store.Species.(*models.MockSpeciesService).Get_ = func(id int64) (*models.Species, error) {
+		if id != want.Id {
+			t.Errorf("wanted request for species %d but got %d", want.Id, id)
+		}
+		calledGet = true
+		return want, nil
+	}
+
+	_, err := apiClient.Species.Get(want.Id)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// if !calledGet {
+	// 	t.Error("!calledGet")
+	// }
+	// if !normalizeDeepEqual(want, got) {
+	// 	t.Errorf("got species %+v but wanted species %+v", got, want)
+	// }
+}
diff --git a/datastore/datastore.go b/datastore/datastore.go
index 43faafd..1919e3a 100644
--- a/datastore/datastore.go
+++ b/datastore/datastore.go
@@ -9,9 +9,10 @@ import (
 
 // A datastore access point (in PostgreSQL)
 type Datastore struct {
-	Users  models.UsersService
-	Genera models.GeneraService
-	dbh    modl.SqlExecutor
+	Users   models.UsersService
+	Genera  models.GeneraService
+	Species models.SpeciesService
+	dbh     modl.SqlExecutor
 }
 
 var (
@@ -29,12 +30,14 @@ func NewDatastore(dbh modl.SqlExecutor) *Datastore {
 	d := &Datastore{dbh: dbh}
 	d.Users = &usersStore{d}
 	d.Genera = &generaStore{d}
+	d.Species = &speciesStore{d}
 	return d
 }
 
 func NewMockDatastore() *Datastore {
 	return &Datastore{
-		Users:  &models.MockUsersService{},
-		Genera: &models.MockGeneraService{},
+		Users:   &models.MockUsersService{},
+		Genera:  &models.MockGeneraService{},
+		Species: &models.MockSpeciesService{},
 	}
 }
diff --git a/datastore/migrations/00003_AddSpecies_down.sql b/datastore/migrations/00003_AddSpecies_down.sql
new file mode 100644
index 0000000..3ce7db5
--- /dev/null
+++ b/datastore/migrations/00003_AddSpecies_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE species;
+
diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/datastore/migrations/00003_AddSpecies_up.sql
new file mode 100644
index 0000000..d290de1
--- /dev/null
+++ b/datastore/migrations/00003_AddSpecies_up.sql
@@ -0,0 +1,15 @@
+-- bactdb
+-- Matthew Dillon
+
+CREATE TABLE species (
+    id BIGSERIAL NOT NULL,
+    genusid BIGINT,
+    speciesname CHARACTER VARYING(100),
+
+    createdat TIMESTAMP WITH TIME ZONE,
+    updatedat TIMESTAMP WITH TIME ZONE,
+    deletedat TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT species_pkey PRIMARY KEY (id),
+    FOREIGN KEY (genusid) REFERENCES genera(id)
+);
diff --git a/datastore/species.go b/datastore/species.go
new file mode 100644
index 0000000..a7f3c17
--- /dev/null
+++ b/datastore/species.go
@@ -0,0 +1,22 @@
+package datastore
+
+import "github.com/thermokarst/bactdb/models"
+
+func init() {
+	DB.AddTableWithName(models.Species{}, "species").SetKeys(true, "Id")
+}
+
+type speciesStore struct {
+	*Datastore
+}
+
+func (s *speciesStore) Get(id int64) (*models.Species, error) {
+	var species []*models.Species
+	if err := s.dbh.Select(&species, `SELECT * FROM species WHERE id=$1;`, id); err != nil {
+		return nil, err
+	}
+	if len(species) == 0 {
+		return nil, models.ErrSpeciesNotFound
+	}
+	return species[0], nil
+}
diff --git a/datastore/species_test.go b/datastore/species_test.go
new file mode 100644
index 0000000..915d522
--- /dev/null
+++ b/datastore/species_test.go
@@ -0,0 +1,37 @@
+package datastore
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/thermokarst/bactdb/models"
+)
+
+func TestSpeciesStore_Get_db(t *testing.T) {
+	tx, _ := DB.Begin()
+	defer tx.Rollback()
+
+	// Test on a clean database
+	tx.Exec(`DELETE FROM species;`)
+
+	wantGenus := &models.Genus{GenusName: "Test Genus"}
+	if err := tx.Insert(wantGenus); err != nil {
+		t.Fatal(err)
+	}
+
+	want := &models.Species{Id: 1, GenusId: wantGenus.Id, SpeciesName: "Test Species"}
+	if err := tx.Insert(want); err != nil {
+		t.Fatal(err)
+	}
+
+	d := NewDatastore(tx)
+	species, err := d.Species.Get(1)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
+	if !reflect.DeepEqual(species, want) {
+		t.Errorf("got species %+v, want %+v", species, want)
+	}
+}
diff --git a/models/client.go b/models/client.go
index bb321e7..b94f3f1 100644
--- a/models/client.go
+++ b/models/client.go
@@ -16,8 +16,9 @@ import (
 
 // A Client communicates with bactdb's HTTP API.
 type Client struct {
-	Users  UsersService
-	Genera GeneraService
+	Users   UsersService
+	Genera  GeneraService
+	Species SpeciesService
 
 	// BaseURL for HTTP requests to bactdb's API.
 	BaseURL *url.URL
@@ -47,6 +48,7 @@ func NewClient(httpClient *http.Client) *Client {
 	}
 	c.Users = &usersService{c}
 	c.Genera = &generaService{c}
+	c.Species = &speciesService{c}
 	return c
 }
 
diff --git a/models/species.go b/models/species.go
new file mode 100644
index 0000000..336c474
--- /dev/null
+++ b/models/species.go
@@ -0,0 +1,69 @@
+package models
+
+import (
+	"errors"
+	"strconv"
+	"time"
+
+	"github.com/thermokarst/bactdb/router"
+)
+
+// A Species is a high-level classifier in bactdb.
+type Species struct {
+	Id          int64     `json:"id,omitempty"`
+	GenusId     int64     `json:"genus_id"`
+	SpeciesName string    `json:"species_name"`
+	CreatedAt   time.Time `json:"created_at"`
+	UpdatedAt   time.Time `json:"updated_at"`
+	DeletedAt   time.Time `json:"deleted_at"`
+}
+
+// SpeciesService interacts with the species-related endpoints in bactdb's API.
+type SpeciesService interface {
+	// Get a species
+	Get(id int64) (*Species, error)
+}
+
+var (
+	ErrSpeciesNotFound = errors.New("species not found")
+)
+
+type speciesService struct {
+	client *Client
+}
+
+func (s *speciesService) Get(id int64) (*Species, error) {
+	// Pass in key value pairs as strings, sp that the gorilla mux URL generation is happy
+	strId := strconv.FormatInt(id, 10)
+
+	url, err := s.client.url(router.Species, map[string]string{"Id": strId}, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", url.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var species *Species
+	_, err = s.client.Do(req, &species)
+	if err != nil {
+		return nil, err
+	}
+
+	return species, nil
+}
+
+type MockSpeciesService struct {
+	Get_ func(id int64) (*Species, error)
+}
+
+var _ SpeciesService = &MockSpeciesService{}
+
+func (s *MockSpeciesService) Get(id int64) (*Species, error) {
+	if s.Get_ == nil {
+		return nil, nil
+	}
+	return s.Get_(id)
+}
diff --git a/models/species_test.go b/models/species_test.go
new file mode 100644
index 0000000..e080179
--- /dev/null
+++ b/models/species_test.go
@@ -0,0 +1,39 @@
+package models
+
+import (
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/thermokarst/bactdb/router"
+)
+
+func TestSpeciesService_Get(t *testing.T) {
+	setup()
+	defer teardown()
+
+	want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	var called bool
+	mux.HandleFunc(urlPath(t, router.Species, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) {
+		called = true
+		testMethod(t, r, "GET")
+
+		writeJSON(w, want)
+	})
+
+	species, err := client.Species.Get(1)
+	if err != nil {
+		t.Errorf("Species.Get returned error: %v", err)
+	}
+
+	if !called {
+		t.Fatal("!called")
+	}
+
+	normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
+
+	if !reflect.DeepEqual(species, want) {
+		t.Errorf("Species.Get returned %+v, want %+v", species, want)
+	}
+}
diff --git a/router/api.go b/router/api.go
index 1df34ee..229bddc 100644
--- a/router/api.go
+++ b/router/api.go
@@ -16,5 +16,9 @@ func API() *mux.Router {
 	m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus)
 	m.Path("/genera/{Id:.+}").Methods("PUT").Name(UpdateGenus)
 	m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus)
+
+	// Species
+	m.Path("/species/{Id:.+}").Methods("GET").Name(Species)
+
 	return m
 }
diff --git a/router/routes.go b/router/routes.go
index e3bb7bb..9c4ed37 100644
--- a/router/routes.go
+++ b/router/routes.go
@@ -10,4 +10,6 @@ const (
 	Genera      = "genera"
 	UpdateGenus = "genus:update"
 	DeleteGenus = "genus:delete"
+
+	Species = "species"
 )

From ed2ba266544498e39cd0fb241b1e555cfaf8e8a2 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Wed, 15 Oct 2014 14:11:17 -0800
Subject: [PATCH 2/9] Removed commented portion

---
 api/species_test.go | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/api/species_test.go b/api/species_test.go
index 5d1bf87..407a952 100644
--- a/api/species_test.go
+++ b/api/species_test.go
@@ -20,15 +20,15 @@ func TestSpecies_Get(t *testing.T) {
 		return want, nil
 	}
 
-	_, err := apiClient.Species.Get(want.Id)
+	got, err := apiClient.Species.Get(want.Id)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	// if !calledGet {
-	// 	t.Error("!calledGet")
-	// }
-	// if !normalizeDeepEqual(want, got) {
-	// 	t.Errorf("got species %+v but wanted species %+v", got, want)
-	// }
+	if !calledGet {
+		t.Error("!calledGet")
+	}
+	if !normalizeDeepEqual(want, got) {
+		t.Errorf("got species %+v but wanted species %+v", got, want)
+	}
 }

From 7fe5566edfd22068c1b0394c130f8e4622080cd2 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Wed, 15 Oct 2014 16:43:09 -0800
Subject: [PATCH 3/9] Create species, cleanup schema. - create species -
 species genus_id not null

---
 api/handler.go                               |  1 +
 api/species.go                               | 20 +++++++++++
 api/species_test.go                          | 27 +++++++++++++++
 datastore/migrations/00003_AddSpecies_up.sql |  2 +-
 datastore/species.go                         |  7 ++++
 datastore/species_test.go                    | 28 +++++++++++++++
 models/species.go                            | 33 +++++++++++++++++-
 models/species_test.go                       | 36 ++++++++++++++++++++
 router/api.go                                |  1 +
 router/routes.go                             |  3 +-
 10 files changed, 155 insertions(+), 3 deletions(-)

diff --git a/api/handler.go b/api/handler.go
index 384740a..65bbc79 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -30,6 +30,7 @@ func Handler() *mux.Router {
 	m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus))
 
 	m.Get(router.Species).Handler(handler(serveSpecies))
+	m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies))
 
 	return m
 }
diff --git a/api/species.go b/api/species.go
index c8e127c..b8bae18 100644
--- a/api/species.go
+++ b/api/species.go
@@ -1,10 +1,12 @@
 package api
 
 import (
+	"encoding/json"
 	"net/http"
 	"strconv"
 
 	"github.com/gorilla/mux"
+	"github.com/thermokarst/bactdb/models"
 )
 
 func serveSpecies(w http.ResponseWriter, r *http.Request) error {
@@ -20,3 +22,21 @@ func serveSpecies(w http.ResponseWriter, r *http.Request) error {
 
 	return writeJSON(w, species)
 }
+
+func serveCreateSpecies(w http.ResponseWriter, r *http.Request) error {
+	var species models.Species
+	err := json.NewDecoder(r.Body).Decode(&species)
+	if err != nil {
+		return err
+	}
+
+	created, err := store.Species.Create(&species)
+	if err != nil {
+		return err
+	}
+	if created {
+		w.WriteHeader(http.StatusCreated)
+	}
+
+	return writeJSON(w, species)
+}
diff --git a/api/species_test.go b/api/species_test.go
index 407a952..24fd557 100644
--- a/api/species_test.go
+++ b/api/species_test.go
@@ -32,3 +32,30 @@ func TestSpecies_Get(t *testing.T) {
 		t.Errorf("got species %+v but wanted species %+v", got, want)
 	}
 }
+
+func TestSpecies_Create(t *testing.T) {
+	setup()
+
+	want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	calledPost := false
+	store.Species.(*models.MockSpeciesService).Create_ = func(species *models.Species) (bool, error) {
+		if !normalizeDeepEqual(want, species) {
+			t.Errorf("wanted request for species %d but got %d", want, species)
+		}
+		calledPost = true
+		return true, nil
+	}
+
+	success, err := apiClient.Species.Create(want)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !calledPost {
+		t.Error("!calledPost")
+	}
+	if !success {
+		t.Error("!success")
+	}
+}
diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/datastore/migrations/00003_AddSpecies_up.sql
index d290de1..aff073b 100644
--- a/datastore/migrations/00003_AddSpecies_up.sql
+++ b/datastore/migrations/00003_AddSpecies_up.sql
@@ -3,7 +3,7 @@
 
 CREATE TABLE species (
     id BIGSERIAL NOT NULL,
-    genusid BIGINT,
+    genusid BIGINT NOT NULL,
     speciesname CHARACTER VARYING(100),
 
     createdat TIMESTAMP WITH TIME ZONE,
diff --git a/datastore/species.go b/datastore/species.go
index a7f3c17..f7e6dee 100644
--- a/datastore/species.go
+++ b/datastore/species.go
@@ -20,3 +20,10 @@ func (s *speciesStore) Get(id int64) (*models.Species, error) {
 	}
 	return species[0], nil
 }
+
+func (s *speciesStore) Create(species *models.Species) (bool, error) {
+	if err := s.dbh.Insert(species); err != nil {
+		return false, err
+	}
+	return true, nil
+}
diff --git a/datastore/species_test.go b/datastore/species_test.go
index 915d522..db81db6 100644
--- a/datastore/species_test.go
+++ b/datastore/species_test.go
@@ -35,3 +35,31 @@ func TestSpeciesStore_Get_db(t *testing.T) {
 		t.Errorf("got species %+v, want %+v", species, want)
 	}
 }
+
+func TestSpeciesStore_Create_db(t *testing.T) {
+	tx, _ := DB.Begin()
+	defer tx.Rollback()
+
+	// Test on a clean database
+	tx.Exec(`DELETE FROM species;`)
+
+	genus := &models.Genus{}
+	if err := tx.Insert(genus); err != nil {
+		t.Fatal(err)
+	}
+
+	species := &models.Species{Id: 1, GenusId: genus.Id, SpeciesName: "Test Species"}
+
+	d := NewDatastore(tx)
+	created, err := d.Species.Create(species)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !created {
+		t.Error("!created")
+	}
+	if species.Id == 0 {
+		t.Error("want nonzero species.Id after submitting")
+	}
+}
diff --git a/models/species.go b/models/species.go
index 336c474..3437d8e 100644
--- a/models/species.go
+++ b/models/species.go
@@ -2,6 +2,7 @@ package models
 
 import (
 	"errors"
+	"net/http"
 	"strconv"
 	"time"
 
@@ -22,6 +23,9 @@ type Species struct {
 type SpeciesService interface {
 	// Get a species
 	Get(id int64) (*Species, error)
+
+	// Create a species record
+	Create(species *Species) (bool, error)
 }
 
 var (
@@ -55,8 +59,28 @@ func (s *speciesService) Get(id int64) (*Species, error) {
 	return species, nil
 }
 
+func (s *speciesService) Create(species *Species) (bool, error) {
+	url, err := s.client.url(router.CreateSpecies, nil, nil)
+	if err != nil {
+		return false, err
+	}
+
+	req, err := s.client.NewRequest("POST", url.String(), species)
+	if err != nil {
+		return false, err
+	}
+
+	resp, err := s.client.Do(req, &species)
+	if err != nil {
+		return false, err
+	}
+
+	return resp.StatusCode == http.StatusCreated, nil
+}
+
 type MockSpeciesService struct {
-	Get_ func(id int64) (*Species, error)
+	Get_    func(id int64) (*Species, error)
+	Create_ func(species *Species) (bool, error)
 }
 
 var _ SpeciesService = &MockSpeciesService{}
@@ -67,3 +91,10 @@ func (s *MockSpeciesService) Get(id int64) (*Species, error) {
 	}
 	return s.Get_(id)
 }
+
+func (s *MockSpeciesService) Create(species *Species) (bool, error) {
+	if s.Create_ == nil {
+		return false, nil
+	}
+	return s.Create_(species)
+}
diff --git a/models/species_test.go b/models/species_test.go
index e080179..338d13b 100644
--- a/models/species_test.go
+++ b/models/species_test.go
@@ -37,3 +37,39 @@ func TestSpeciesService_Get(t *testing.T) {
 		t.Errorf("Species.Get returned %+v, want %+v", species, want)
 	}
 }
+
+func TestSpeciesService_Create(t *testing.T) {
+	setup()
+	defer teardown()
+
+	want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	var called bool
+	mux.HandleFunc(urlPath(t, router.CreateSpecies, nil), func(w http.ResponseWriter, r *http.Request) {
+		called = true
+		testMethod(t, r, "POST")
+		testBody(t, r, `{"id":1,"genus_id":1,"species_name":"Test Species","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","deleted_at":"0001-01-01T00:00:00Z"}`+"\n")
+
+		w.WriteHeader(http.StatusCreated)
+		writeJSON(w, want)
+	})
+
+	species := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+	created, err := client.Species.Create(species)
+	if err != nil {
+		t.Errorf("Species.Create returned error: %v", err)
+	}
+
+	if !created {
+		t.Error("!created")
+	}
+
+	if !called {
+		t.Fatal("!called")
+	}
+
+	normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt)
+	if !reflect.DeepEqual(species, want) {
+		t.Errorf("Species.Create returned %+v, want %+v", species, want)
+	}
+}
diff --git a/router/api.go b/router/api.go
index 229bddc..8ae8b02 100644
--- a/router/api.go
+++ b/router/api.go
@@ -18,6 +18,7 @@ func API() *mux.Router {
 	m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus)
 
 	// Species
+	m.Path("/species").Methods("POST").Name(CreateSpecies)
 	m.Path("/species/{Id:.+}").Methods("GET").Name(Species)
 
 	return m
diff --git a/router/routes.go b/router/routes.go
index 9c4ed37..8b2bd70 100644
--- a/router/routes.go
+++ b/router/routes.go
@@ -11,5 +11,6 @@ const (
 	UpdateGenus = "genus:update"
 	DeleteGenus = "genus:delete"
 
-	Species = "species"
+	Species       = "species"
+	CreateSpecies = "species:create"
 )

From 7e74d672ba8f441424a414314b7a1612ba9dbc3b Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Thu, 16 Oct 2014 09:20:25 -0800
Subject: [PATCH 4/9] Using struct attrs to match desired db schema

---
 datastore/migrations/00001_AddUsers_up.sql   |  6 +++---
 datastore/migrations/00002_AddGenera_up.sql  | 10 +++++-----
 datastore/migrations/00003_AddSpecies_up.sql | 12 ++++++------
 models/genera.go                             |  8 ++++----
 models/species.go                            | 10 +++++-----
 models/users.go                              |  8 ++++----
 6 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/datastore/migrations/00001_AddUsers_up.sql b/datastore/migrations/00001_AddUsers_up.sql
index 9dd8907..4ef446a 100644
--- a/datastore/migrations/00001_AddUsers_up.sql
+++ b/datastore/migrations/00001_AddUsers_up.sql
@@ -5,9 +5,9 @@ CREATE TABLE users (
     id BIGSERIAL NOT NULL,
     username CHARACTER VARYING(100),
 
-    createdat TIMESTAMP WITH TIME ZONE,
-    updatedat TIMESTAMP WITH TIME ZONE,
-    deletedat TIMESTAMP WITH TIME ZONE,
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
 
     CONSTRAINT users_pkey PRIMARY KEY (id)
 );
diff --git a/datastore/migrations/00002_AddGenera_up.sql b/datastore/migrations/00002_AddGenera_up.sql
index 3fa4d79..e9ea156 100644
--- a/datastore/migrations/00002_AddGenera_up.sql
+++ b/datastore/migrations/00002_AddGenera_up.sql
@@ -3,11 +3,11 @@
 
 CREATE TABLE genera (
     id BIGSERIAL NOT NULL,
-    genusname CHARACTER VARYING(100),
+    genus_name CHARACTER VARYING(100),
 
-    createdat TIMESTAMP WITH TIME ZONE,
-    updatedat TIMESTAMP WITH TIME ZONE,
-    deletedat TIMESTAMP WITH TIME ZONE,
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
 
     CONSTRAINT genus_pkey PRIMARY KEY (id)
 );
@@ -15,5 +15,5 @@ CREATE TABLE genera (
 CREATE UNIQUE INDEX genusname_idx
     ON genera
     USING btree
-    (genusname COLLATE pg_catalog."default");
+    (genus_name COLLATE pg_catalog."default");
 
diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/datastore/migrations/00003_AddSpecies_up.sql
index aff073b..88a192a 100644
--- a/datastore/migrations/00003_AddSpecies_up.sql
+++ b/datastore/migrations/00003_AddSpecies_up.sql
@@ -3,13 +3,13 @@
 
 CREATE TABLE species (
     id BIGSERIAL NOT NULL,
-    genusid BIGINT NOT NULL,
-    speciesname CHARACTER VARYING(100),
+    genus_id BIGINT NOT NULL,
+    species_name CHARACTER VARYING(100),
 
-    createdat TIMESTAMP WITH TIME ZONE,
-    updatedat TIMESTAMP WITH TIME ZONE,
-    deletedat TIMESTAMP WITH TIME ZONE,
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
 
     CONSTRAINT species_pkey PRIMARY KEY (id),
-    FOREIGN KEY (genusid) REFERENCES genera(id)
+    FOREIGN KEY (genus_id) REFERENCES genera(id)
 );
diff --git a/models/genera.go b/models/genera.go
index fb87694..60cc2b0 100644
--- a/models/genera.go
+++ b/models/genera.go
@@ -12,10 +12,10 @@ import (
 // A Genus is a high-level classifier in bactdb.
 type Genus struct {
 	Id        int64     `json:"id,omitempty"`
-	GenusName string    `json:"genus_name"`
-	CreatedAt time.Time `json:"created_at"`
-	UpdatedAt time.Time `json:"updated_at"`
-	DeletedAt time.Time `json:"deleted_at"`
+	GenusName string    `db:"genus_name" json:"genus_name"`
+	CreatedAt time.Time `db:"created_at" json:"created_at"`
+	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
+	DeletedAt time.Time `db:"deleted_at" json:"deleted_at"`
 }
 
 // GeneraService interacts with the genus-related endpoints in bactdb's API.
diff --git a/models/species.go b/models/species.go
index 3437d8e..dbd696d 100644
--- a/models/species.go
+++ b/models/species.go
@@ -12,11 +12,11 @@ import (
 // A Species is a high-level classifier in bactdb.
 type Species struct {
 	Id          int64     `json:"id,omitempty"`
-	GenusId     int64     `json:"genus_id"`
-	SpeciesName string    `json:"species_name"`
-	CreatedAt   time.Time `json:"created_at"`
-	UpdatedAt   time.Time `json:"updated_at"`
-	DeletedAt   time.Time `json:"deleted_at"`
+	GenusId     int64     `db:"genus_id" json:"genus_id"`
+	SpeciesName string    `db:"species_name" json:"species_name"`
+	CreatedAt   time.Time `db:"created_at" json:"created_at"`
+	UpdatedAt   time.Time `db:"updated_at" json:"updated_at"`
+	DeletedAt   time.Time `db:"deleted_at" json:"deleted_at"`
 }
 
 // SpeciesService interacts with the species-related endpoints in bactdb's API.
diff --git a/models/users.go b/models/users.go
index e398295..d7a526d 100644
--- a/models/users.go
+++ b/models/users.go
@@ -12,10 +12,10 @@ import (
 // A User is a person that has administrative access to bactdb.
 type User struct {
 	Id        int64     `json:"id,omitempty"`
-	UserName  string    `sql:"size:100" json:"user_name"`
-	CreatedAt time.Time `json:"created_at"`
-	UpdatedAt time.Time `json:"updated_at"`
-	DeletedAt time.Time `json:"deleted_at"`
+	UserName  string    `json:"user_name"`
+	CreatedAt time.Time `db:"created_at" json:"created_at"`
+	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
+	DeletedAt time.Time `db:"deleted_at" json:"deleted_at"`
 }
 
 // UsersService interacts with the user-related endpoints in bactdb's API.

From b045ded9cd97b1b47b868b2047a695c2c443fc78 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Thu, 16 Oct 2014 09:41:01 -0800
Subject: [PATCH 5/9] Importing base schema

---
 datastore/migrations/00004_AddStrain_down.sql |  5 +++++
 datastore/migrations/00004_AddStrain_up.sql   | 20 +++++++++++++++++++
 .../00005_AddObservationTypes_down.sql        |  5 +++++
 .../00005_AddObservationTypes_up.sql          | 14 +++++++++++++
 .../migrations/00006_AddObservations_down.sql |  5 +++++
 .../migrations/00006_AddObservations_up.sql   | 16 +++++++++++++++
 .../00007_AddStrainsObservations_down.sql     |  5 +++++
 .../00007_AddStrainsObservations_up.sql       | 17 ++++++++++++++++
 .../00008_AddText_Measurements_down.sql       |  5 +++++
 .../00008_AddText_Measurements_up.sql         | 14 +++++++++++++
 .../migrations/00009_AddUnit_Types_down.sql   |  5 +++++
 .../migrations/00009_AddUnit_Types_up.sql     | 15 ++++++++++++++
 .../00010_AddNumerical_Measurements_down.sql  |  5 +++++
 .../00010_AddNumerical_Measurements_up.sql    | 17 ++++++++++++++++
 .../00011_AddStrainObsMeasurements_down.sql   |  5 +++++
 .../00011_AddStrainObsMeasurements_up.sql     | 17 ++++++++++++++++
 16 files changed, 170 insertions(+)
 create mode 100644 datastore/migrations/00004_AddStrain_down.sql
 create mode 100644 datastore/migrations/00004_AddStrain_up.sql
 create mode 100644 datastore/migrations/00005_AddObservationTypes_down.sql
 create mode 100644 datastore/migrations/00005_AddObservationTypes_up.sql
 create mode 100644 datastore/migrations/00006_AddObservations_down.sql
 create mode 100644 datastore/migrations/00006_AddObservations_up.sql
 create mode 100644 datastore/migrations/00007_AddStrainsObservations_down.sql
 create mode 100644 datastore/migrations/00007_AddStrainsObservations_up.sql
 create mode 100644 datastore/migrations/00008_AddText_Measurements_down.sql
 create mode 100644 datastore/migrations/00008_AddText_Measurements_up.sql
 create mode 100644 datastore/migrations/00009_AddUnit_Types_down.sql
 create mode 100644 datastore/migrations/00009_AddUnit_Types_up.sql
 create mode 100644 datastore/migrations/00010_AddNumerical_Measurements_down.sql
 create mode 100644 datastore/migrations/00010_AddNumerical_Measurements_up.sql
 create mode 100644 datastore/migrations/00011_AddStrainObsMeasurements_down.sql
 create mode 100644 datastore/migrations/00011_AddStrainObsMeasurements_up.sql

diff --git a/datastore/migrations/00004_AddStrain_down.sql b/datastore/migrations/00004_AddStrain_down.sql
new file mode 100644
index 0000000..94edb83
--- /dev/null
+++ b/datastore/migrations/00004_AddStrain_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE strains;
+
diff --git a/datastore/migrations/00004_AddStrain_up.sql b/datastore/migrations/00004_AddStrain_up.sql
new file mode 100644
index 0000000..855f3a9
--- /dev/null
+++ b/datastore/migrations/00004_AddStrain_up.sql
@@ -0,0 +1,20 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE strains (
+    id BIGSERIAL NOT NULL,
+    species_id BIGINT,
+    strain_name CHARACTER VARYING(100),
+    strain_type CHARACTER VARYING(100),
+    etymology CHARACTER VARYING(500),
+    accession_banks CHARACTER VARYING(100),
+    genbank_embl_ddb CHARACTER VARYING(100),
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT strain_pkey PRIMARY KEY (id),
+    FOREIGN KEY (species_id) REFERENCES species(id)
+);
+
diff --git a/datastore/migrations/00005_AddObservationTypes_down.sql b/datastore/migrations/00005_AddObservationTypes_down.sql
new file mode 100644
index 0000000..39f6366
--- /dev/null
+++ b/datastore/migrations/00005_AddObservationTypes_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE observation_types;
+
diff --git a/datastore/migrations/00005_AddObservationTypes_up.sql b/datastore/migrations/00005_AddObservationTypes_up.sql
new file mode 100644
index 0000000..d18d8b5
--- /dev/null
+++ b/datastore/migrations/00005_AddObservationTypes_up.sql
@@ -0,0 +1,14 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE observation_types (
+    id BIGSERIAL NOT NULL,
+    observation_type_name CHARACTER VARYING(100),
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT observation_types_pkey PRIMARY KEY (id)
+);
+
diff --git a/datastore/migrations/00006_AddObservations_down.sql b/datastore/migrations/00006_AddObservations_down.sql
new file mode 100644
index 0000000..5b5ef85
--- /dev/null
+++ b/datastore/migrations/00006_AddObservations_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE observations;
+
diff --git a/datastore/migrations/00006_AddObservations_up.sql b/datastore/migrations/00006_AddObservations_up.sql
new file mode 100644
index 0000000..df0a1be
--- /dev/null
+++ b/datastore/migrations/00006_AddObservations_up.sql
@@ -0,0 +1,16 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE observations (
+    id BIGSERIAL NOT NULL,
+    observation_name CHARACTER VARYING(100),
+    observation_type_id BIGINT,
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT observations_pkey PRIMARY KEY (id),
+    FOREIGN KEY (observation_type_id) REFERENCES observation_types(id)
+);
+
diff --git a/datastore/migrations/00007_AddStrainsObservations_down.sql b/datastore/migrations/00007_AddStrainsObservations_down.sql
new file mode 100644
index 0000000..c67ba07
--- /dev/null
+++ b/datastore/migrations/00007_AddStrainsObservations_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE strainsobservations;
+
diff --git a/datastore/migrations/00007_AddStrainsObservations_up.sql b/datastore/migrations/00007_AddStrainsObservations_up.sql
new file mode 100644
index 0000000..4c939be
--- /dev/null
+++ b/datastore/migrations/00007_AddStrainsObservations_up.sql
@@ -0,0 +1,17 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE strainsobservations (
+    id BIGSERIAL NOT NULL,
+    strain_id BIGINT NOT NULL,
+    observations_id BIGINT NOT NULL,
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT strainsobservations_pkey PRIMARY KEY (id),
+    FOREIGN KEY (strain_id) REFERENCES strains(id),
+    FOREIGN KEY (observations_id) REFERENCES observations(id)
+);
+
diff --git a/datastore/migrations/00008_AddText_Measurements_down.sql b/datastore/migrations/00008_AddText_Measurements_down.sql
new file mode 100644
index 0000000..3b8bc9b
--- /dev/null
+++ b/datastore/migrations/00008_AddText_Measurements_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE text_measurements;
+
diff --git a/datastore/migrations/00008_AddText_Measurements_up.sql b/datastore/migrations/00008_AddText_Measurements_up.sql
new file mode 100644
index 0000000..d0992ce
--- /dev/null
+++ b/datastore/migrations/00008_AddText_Measurements_up.sql
@@ -0,0 +1,14 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE text_measurements (
+    id BIGSERIAL NOT NULL,
+    text_measurement_name CHARACTER VARYING(100),
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT text_measurements_pkey PRIMARY KEY (id)
+);
+
diff --git a/datastore/migrations/00009_AddUnit_Types_down.sql b/datastore/migrations/00009_AddUnit_Types_down.sql
new file mode 100644
index 0000000..ffbb77c
--- /dev/null
+++ b/datastore/migrations/00009_AddUnit_Types_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE unit_types;
+
diff --git a/datastore/migrations/00009_AddUnit_Types_up.sql b/datastore/migrations/00009_AddUnit_Types_up.sql
new file mode 100644
index 0000000..348242a
--- /dev/null
+++ b/datastore/migrations/00009_AddUnit_Types_up.sql
@@ -0,0 +1,15 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE unit_types (
+    id BIGSERIAL NOT NULL,
+    name CHARACTER VARYING(100),
+    symbol CHARACTER VARYING(10),
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT unit_types_pkey PRIMARY KEY (id)
+);
+
diff --git a/datastore/migrations/00010_AddNumerical_Measurements_down.sql b/datastore/migrations/00010_AddNumerical_Measurements_down.sql
new file mode 100644
index 0000000..b8fbc65
--- /dev/null
+++ b/datastore/migrations/00010_AddNumerical_Measurements_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE numerical_measurements;
+
diff --git a/datastore/migrations/00010_AddNumerical_Measurements_up.sql b/datastore/migrations/00010_AddNumerical_Measurements_up.sql
new file mode 100644
index 0000000..b1deda2
--- /dev/null
+++ b/datastore/migrations/00010_AddNumerical_Measurements_up.sql
@@ -0,0 +1,17 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE numerical_measurements (
+    id BIGSERIAL NOT NULL,
+    measurement_value NUMERIC(6, 4) NOT NULL,
+    confidence_interval NUMERIC(6,4) NULL,
+    unit_type_id BIGINT,
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT numerical_measurements_pkey PRIMARY KEY (id),
+    FOREIGN KEY (unit_type_id) REFERENCES unit_types(id)
+);
+
diff --git a/datastore/migrations/00011_AddStrainObsMeasurements_down.sql b/datastore/migrations/00011_AddStrainObsMeasurements_down.sql
new file mode 100644
index 0000000..815b20d
--- /dev/null
+++ b/datastore/migrations/00011_AddStrainObsMeasurements_down.sql
@@ -0,0 +1,5 @@
+-- bactdb
+-- Matthew R Dillon
+
+DROP TABLE strainsobsmeasurements;
+
diff --git a/datastore/migrations/00011_AddStrainObsMeasurements_up.sql b/datastore/migrations/00011_AddStrainObsMeasurements_up.sql
new file mode 100644
index 0000000..9abf103
--- /dev/null
+++ b/datastore/migrations/00011_AddStrainObsMeasurements_up.sql
@@ -0,0 +1,17 @@
+-- bactdb
+-- Matthew R Dillon
+
+CREATE TABLE strainsobsmeasurements (
+    id BIGSERIAL NOT NULL,
+    strainsobservations_id BIGINT,
+    measurement_table CHARACTER VARYING(15),
+    measurement_id BIGINT,
+
+    created_at TIMESTAMP WITH TIME ZONE,
+    updated_at TIMESTAMP WITH TIME ZONE,
+    deleted_at TIMESTAMP WITH TIME ZONE,
+
+    CONSTRAINT strainsobsmeasurements_pkey PRIMARY KEY (id),
+    FOREIGN KEY (strainsobservations_id) REFERENCES strainsobservations(id)
+);
+

From 5244ae529a5c9cf00f2da6d8be21a14a850e6b8a Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Thu, 23 Oct 2014 17:29:07 -0800
Subject: [PATCH 6/9] Species: List species.

---
 api/handler.go            |  1 +
 api/species.go            | 17 +++++++++++++++++
 api/species_test.go       | 29 +++++++++++++++++++++++++++++
 datastore/species.go      | 12 ++++++++++++
 datastore/species_test.go | 33 +++++++++++++++++++++++++++++++++
 models/species.go         | 35 +++++++++++++++++++++++++++++++++++
 models/species_test.go    | 32 ++++++++++++++++++++++++++++++++
 router/api.go             |  1 +
 router/routes.go          |  1 +
 9 files changed, 161 insertions(+)

diff --git a/api/handler.go b/api/handler.go
index 65bbc79..98c8952 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -31,6 +31,7 @@ func Handler() *mux.Router {
 
 	m.Get(router.Species).Handler(handler(serveSpecies))
 	m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies))
+	m.Get(router.SpeciesList).Handler(handler(serveSpeciesList))
 
 	return m
 }
diff --git a/api/species.go b/api/species.go
index b8bae18..7ff94ff 100644
--- a/api/species.go
+++ b/api/species.go
@@ -40,3 +40,20 @@ func serveCreateSpecies(w http.ResponseWriter, r *http.Request) error {
 
 	return writeJSON(w, species)
 }
+
+func serveSpeciesList(w http.ResponseWriter, r *http.Request) error {
+	var opt models.SpeciesListOptions
+	if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil {
+		return err
+	}
+
+	species, err := store.Species.List(&opt)
+	if err != nil {
+		return err
+	}
+	if species == nil {
+		species = []*models.Species{}
+	}
+
+	return writeJSON(w, species)
+}
diff --git a/api/species_test.go b/api/species_test.go
index 24fd557..490c62c 100644
--- a/api/species_test.go
+++ b/api/species_test.go
@@ -59,3 +59,32 @@ func TestSpecies_Create(t *testing.T) {
 		t.Error("!success")
 	}
 }
+
+func TestSpecies_List(t *testing.T) {
+	setup()
+
+	want := []*models.Species{{Id: 1, GenusId: 1, SpeciesName: "Test Species"}}
+	wantOpt := &models.SpeciesListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}
+
+	calledList := false
+	store.Species.(*models.MockSpeciesService).List_ = func(opt *models.SpeciesListOptions) ([]*models.Species, error) {
+		if !normalizeDeepEqual(wantOpt, opt) {
+			t.Errorf("wanted options %d but got %d", wantOpt, opt)
+		}
+		calledList = true
+		return want, nil
+	}
+
+	species, err := apiClient.Species.List(wantOpt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !calledList {
+		t.Error("!calledList")
+	}
+
+	if !normalizeDeepEqual(&want, &species) {
+		t.Errorf("got species %+v but wanted species %+v", species, want)
+	}
+}
diff --git a/datastore/species.go b/datastore/species.go
index f7e6dee..cccdd07 100644
--- a/datastore/species.go
+++ b/datastore/species.go
@@ -27,3 +27,15 @@ func (s *speciesStore) Create(species *models.Species) (bool, error) {
 	}
 	return true, nil
 }
+
+func (s *speciesStore) List(opt *models.SpeciesListOptions) ([]*models.Species, error) {
+	if opt == nil {
+		opt = &models.SpeciesListOptions{}
+	}
+	var species []*models.Species
+	err := s.dbh.Select(&species, `SELECT * FROM species LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset())
+	if err != nil {
+		return nil, err
+	}
+	return species, nil
+}
diff --git a/datastore/species_test.go b/datastore/species_test.go
index db81db6..a540a99 100644
--- a/datastore/species_test.go
+++ b/datastore/species_test.go
@@ -63,3 +63,36 @@ func TestSpeciesStore_Create_db(t *testing.T) {
 		t.Error("want nonzero species.Id after submitting")
 	}
 }
+
+func TestSpeciesStore_List_db(t *testing.T) {
+	tx, _ := DB.Begin()
+	defer tx.Rollback()
+
+	// Test on a clean database
+	tx.Exec(`DELETE FROM species;`)
+
+	genus := &models.Genus{}
+
+	if err := tx.Insert(genus); err != nil {
+		t.Fatal(err)
+	}
+
+	want := []*models.Species{{Id: 1, GenusId: genus.Id, SpeciesName: "Test Species"}}
+
+	if err := tx.Insert(want[0]); err != nil {
+		t.Fatal(err)
+	}
+
+	d := NewDatastore(tx)
+	species, err := d.Species.List(&models.SpeciesListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, g := range want {
+		normalizeTime(&g.CreatedAt, &g.UpdatedAt, &g.DeletedAt)
+	}
+	if !reflect.DeepEqual(species, want) {
+		t.Errorf("got species %+v, want %+v", species, want)
+	}
+}
diff --git a/models/species.go b/models/species.go
index dbd696d..faf6820 100644
--- a/models/species.go
+++ b/models/species.go
@@ -24,6 +24,9 @@ type SpeciesService interface {
 	// Get a species
 	Get(id int64) (*Species, error)
 
+	// List all species
+	List(opt *SpeciesListOptions) ([]*Species, error)
+
 	// Create a species record
 	Create(species *Species) (bool, error)
 }
@@ -78,8 +81,33 @@ func (s *speciesService) Create(species *Species) (bool, error) {
 	return resp.StatusCode == http.StatusCreated, nil
 }
 
+type SpeciesListOptions struct {
+	ListOptions
+}
+
+func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) {
+	url, err := s.client.url(router.SpeciesList, nil, opt)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", url.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var species []*Species
+	_, err = s.client.Do(req, &species)
+	if err != nil {
+		return nil, err
+	}
+
+	return species, nil
+}
+
 type MockSpeciesService struct {
 	Get_    func(id int64) (*Species, error)
+	List_   func(opt *SpeciesListOptions) ([]*Species, error)
 	Create_ func(species *Species) (bool, error)
 }
 
@@ -98,3 +126,10 @@ func (s *MockSpeciesService) Create(species *Species) (bool, error) {
 	}
 	return s.Create_(species)
 }
+
+func (s *MockSpeciesService) List(opt *SpeciesListOptions) ([]*Species, error) {
+	if s.List_ == nil {
+		return nil, nil
+	}
+	return s.List_(opt)
+}
diff --git a/models/species_test.go b/models/species_test.go
index 338d13b..4e6873b 100644
--- a/models/species_test.go
+++ b/models/species_test.go
@@ -73,3 +73,35 @@ func TestSpeciesService_Create(t *testing.T) {
 		t.Errorf("Species.Create returned %+v, want %+v", species, want)
 	}
 }
+
+func TestSpeciesService_List(t *testing.T) {
+	setup()
+	defer teardown()
+
+	want := []*Species{{Id: 1, GenusId: 1, SpeciesName: "Test Species"}}
+
+	var called bool
+	mux.HandleFunc(urlPath(t, router.SpeciesList, nil), func(w http.ResponseWriter, r *http.Request) {
+		called = true
+		testMethod(t, r, "GET")
+		testFormValues(t, r, values{})
+
+		writeJSON(w, want)
+	})
+
+	species, err := client.Species.List(nil)
+	if err != nil {
+		t.Errorf("Species.List returned error: %v", err)
+	}
+
+	if !called {
+		t.Fatal("!called")
+	}
+
+	for _, u := range want {
+		normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt)
+	}
+	if !reflect.DeepEqual(species, want) {
+		t.Errorf("Species.List return %+v, want %+v", species, want)
+	}
+}
diff --git a/router/api.go b/router/api.go
index 8ae8b02..2ffa19a 100644
--- a/router/api.go
+++ b/router/api.go
@@ -18,6 +18,7 @@ func API() *mux.Router {
 	m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus)
 
 	// Species
+	m.Path("/species").Methods("GET").Name(SpeciesList)
 	m.Path("/species").Methods("POST").Name(CreateSpecies)
 	m.Path("/species/{Id:.+}").Methods("GET").Name(Species)
 
diff --git a/router/routes.go b/router/routes.go
index 8b2bd70..4d7a9ac 100644
--- a/router/routes.go
+++ b/router/routes.go
@@ -13,4 +13,5 @@ const (
 
 	Species       = "species"
 	CreateSpecies = "species:create"
+	SpeciesList   = "species:list"
 )

From c8d1d0a84f589da0884b4e3a52a58e2d06d0dc9f Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Thu, 23 Oct 2014 18:25:15 -0800
Subject: [PATCH 7/9] species update wip

---
 datastore/species_test.go | 35 +++++++++++++++++++++++++++++++++++
 models/species.go         | 32 ++++++++++++++++++++++++++++++++
 models/species_test.go    | 31 +++++++++++++++++++++++++++++++
 router/api.go             |  1 +
 router/routes.go          |  1 +
 5 files changed, 100 insertions(+)

diff --git a/datastore/species_test.go b/datastore/species_test.go
index a540a99..62235ba 100644
--- a/datastore/species_test.go
+++ b/datastore/species_test.go
@@ -96,3 +96,38 @@ func TestSpeciesStore_List_db(t *testing.T) {
 		t.Errorf("got species %+v, want %+v", species, want)
 	}
 }
+
+func TestSpeciesStore_Update_db(t *testing.T) {
+	tx, _ := DB.Begin()
+	defer tx.Rollback()
+
+	// Test on a clean database
+	tx.Exec(`DELETE FROM species;`)
+
+	d := NewDatastore(nil)
+	// Add a new record
+	genus := &models.Genus{GenusName: "Test Genus"}
+	_, err := d.Genera.Create(genus)
+	if err != nil {
+		t.Fatal(err)
+	}
+	created := &model.Species{GenusId: genus.Id, SpeciesName: "Test Species"}
+	_, err := d.Species.Create(created)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !created {
+		t.Error("!created")
+	}
+
+	// Tweak it
+	species.SpeciesName = "Updated Species"
+	updated, err := d.Species.Update(species.Id, species)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !updated {
+		t.Error("!updated")
+	}
+}
diff --git a/models/species.go b/models/species.go
index faf6820..b742de8 100644
--- a/models/species.go
+++ b/models/species.go
@@ -29,6 +29,9 @@ type SpeciesService interface {
 
 	// Create a species record
 	Create(species *Species) (bool, error)
+
+	// Update an existing species
+	Update(id int64, species *Species) (updated bool, err error)
 }
 
 var (
@@ -105,10 +108,32 @@ func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) {
 	return species, nil
 }
 
+func (s *speciesService) Update(id int64, species *Species) (bool, error) {
+	strId := strconv.FormatInt(id, 10)
+
+	url, err := s.client.url(router.UpdateSpecies, map[string]string{"Id": strId}, nil)
+	if err != nil {
+		return false, err
+	}
+
+	req, err := s.client.NewRequest("PUT", url.String(), species)
+	if err != nil {
+		return false, err
+	}
+
+	resp, err := s.client.Do(req, &species)
+	if err != nil {
+		return false, err
+	}
+
+	return resp.StatusCode == http.StatusOK, nil
+}
+
 type MockSpeciesService struct {
 	Get_    func(id int64) (*Species, error)
 	List_   func(opt *SpeciesListOptions) ([]*Species, error)
 	Create_ func(species *Species) (bool, error)
+	Update_ func(id int64, species *Species) (bool, error)
 }
 
 var _ SpeciesService = &MockSpeciesService{}
@@ -133,3 +158,10 @@ func (s *MockSpeciesService) List(opt *SpeciesListOptions) ([]*Species, error) {
 	}
 	return s.List_(opt)
 }
+
+func (s *MockSpeciesService) Update(id int64, species *Species) (bool, error) {
+	if s.Update_ == nil {
+		return false, nil
+	}
+	return s.Update_(id, species)
+}
diff --git a/models/species_test.go b/models/species_test.go
index 4e6873b..cb66f9e 100644
--- a/models/species_test.go
+++ b/models/species_test.go
@@ -105,3 +105,34 @@ func TestSpeciesService_List(t *testing.T) {
 		t.Errorf("Species.List return %+v, want %+v", species, want)
 	}
 }
+
+func TestSpeciesService_Update(t *testing.T) {
+	setup()
+	defer teardown()
+
+	want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	var called bool
+	mux.HandleFunc(urlPath(t, router.UpdateSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) {
+		called = true
+		testMethod(t, r, "PUT")
+		testBody(t, r, `{"id":1,"genus_id":1,"species_name":"Test Species Updated","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","deleted_at":"0001-01-01T00:00:00Z"}`+"\n")
+
+		w.WriteHeader(http.StatusOK)
+		writeJSON(w, want)
+	})
+
+	species := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species Updated"}
+	updated, err := client.Species.Update(1, species)
+	if err != nil {
+		t.Errorf("Species.Update returned error: %v", err)
+	}
+
+	if !updated {
+		t.Error("!updated")
+	}
+
+	if !called {
+		t.Fatal("!called")
+	}
+}
diff --git a/router/api.go b/router/api.go
index 2ffa19a..5bf24d2 100644
--- a/router/api.go
+++ b/router/api.go
@@ -21,6 +21,7 @@ func API() *mux.Router {
 	m.Path("/species").Methods("GET").Name(SpeciesList)
 	m.Path("/species").Methods("POST").Name(CreateSpecies)
 	m.Path("/species/{Id:.+}").Methods("GET").Name(Species)
+	m.Path("/species/{Id:.+}").Methods("PUT").Name(UpdateSpecies)
 
 	return m
 }
diff --git a/router/routes.go b/router/routes.go
index 4d7a9ac..0db9205 100644
--- a/router/routes.go
+++ b/router/routes.go
@@ -14,4 +14,5 @@ const (
 	Species       = "species"
 	CreateSpecies = "species:create"
 	SpeciesList   = "species:list"
+	UpdateSpecies = "species:update"
 )

From 22d2b8b41dfe2afb644026f863aef5cb37efcbe3 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Fri, 24 Oct 2014 10:08:12 -0800
Subject: [PATCH 8/9] Finishing up changes from previous commit

---
 api/handler.go            |  1 +
 api/species.go            | 19 +++++++++++++++++++
 api/species_test.go       | 30 ++++++++++++++++++++++++++++++
 datastore/species.go      | 22 ++++++++++++++++++++++
 datastore/species_test.go |  4 ++--
 5 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/api/handler.go b/api/handler.go
index 98c8952..b7999d8 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -32,6 +32,7 @@ func Handler() *mux.Router {
 	m.Get(router.Species).Handler(handler(serveSpecies))
 	m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies))
 	m.Get(router.SpeciesList).Handler(handler(serveSpeciesList))
+	m.Get(router.UpdateSpecies).Handler(handler(serveUpdateSpecies))
 
 	return m
 }
diff --git a/api/species.go b/api/species.go
index 7ff94ff..1e218a4 100644
--- a/api/species.go
+++ b/api/species.go
@@ -57,3 +57,22 @@ func serveSpeciesList(w http.ResponseWriter, r *http.Request) error {
 
 	return writeJSON(w, species)
 }
+
+func serveUpdateSpecies(w http.ResponseWriter, r *http.Request) error {
+	id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
+	var species models.Species
+	err := json.NewDecoder(r.Body).Decode(&species)
+	if err != nil {
+		return err
+	}
+
+	updated, err := store.Species.Update(id, &species)
+	if err != nil {
+		return err
+	}
+	if updated {
+		w.WriteHeader(http.StatusOK)
+	}
+
+	return writeJSON(w, species)
+}
diff --git a/api/species_test.go b/api/species_test.go
index 490c62c..a0c003a 100644
--- a/api/species_test.go
+++ b/api/species_test.go
@@ -88,3 +88,33 @@ func TestSpecies_List(t *testing.T) {
 		t.Errorf("got species %+v but wanted species %+v", species, want)
 	}
 }
+
+func TestSpecies_Update(t *testing.T) {
+	setup()
+
+	want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	calledPut := false
+	store.Species.(*models.MockSpeciesService).Update_ = func(id int64, species *models.Species) (bool, error) {
+		if id != want.Id {
+			t.Errorf("wanted request for species %d but got %d", want.Id, id)
+		}
+		if !normalizeDeepEqual(want, species) {
+			t.Errorf("wanted request for species %d but got %d", want, species)
+		}
+		calledPut = true
+		return true, nil
+	}
+
+	success, err := apiClient.Species.Update(1, want)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !calledPut {
+		t.Error("!calledPut")
+	}
+	if !success {
+		t.Error("!success")
+	}
+}
diff --git a/datastore/species.go b/datastore/species.go
index cccdd07..c3087c4 100644
--- a/datastore/species.go
+++ b/datastore/species.go
@@ -39,3 +39,25 @@ func (s *speciesStore) List(opt *models.SpeciesListOptions) ([]*models.Species,
 	}
 	return species, nil
 }
+
+func (s *speciesStore) Update(id int64, species *models.Species) (bool, error) {
+	_, err := s.Get(id)
+	if err != nil {
+		return false, err
+	}
+
+	if id != species.Id {
+		return false, models.ErrSpeciesNotFound
+	}
+
+	changed, err := s.dbh.Update(species)
+	if err != nil {
+		return false, err
+	}
+
+	if changed == 0 {
+		return false, ErrNoRowsUpdated
+	}
+
+	return true, nil
+}
diff --git a/datastore/species_test.go b/datastore/species_test.go
index 62235ba..1d7c3cd 100644
--- a/datastore/species_test.go
+++ b/datastore/species_test.go
@@ -111,8 +111,8 @@ func TestSpeciesStore_Update_db(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	created := &model.Species{GenusId: genus.Id, SpeciesName: "Test Species"}
-	_, err := d.Species.Create(created)
+	species := &models.Species{GenusId: genus.Id, SpeciesName: "Test Species"}
+	created, err := d.Species.Create(species)
 	if err != nil {
 		t.Fatal(err)
 	}

From 4669aff3c2f577eb171455351bf38c1f8ef61db3 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <mrdillon@alaska.edu>
Date: Fri, 24 Oct 2014 10:42:31 -0800
Subject: [PATCH 9/9] Species - delete

---
 api/handler.go            |  1 +
 api/species.go            | 14 ++++++++++++++
 api/species_test.go       | 27 +++++++++++++++++++++++++++
 datastore/species.go      | 16 ++++++++++++++++
 datastore/species_test.go | 39 +++++++++++++++++++++++++++++++++++++++
 models/species.go         | 33 +++++++++++++++++++++++++++++++++
 models/species_test.go    | 29 +++++++++++++++++++++++++++++
 router/api.go             |  1 +
 router/routes.go          |  1 +
 9 files changed, 161 insertions(+)

diff --git a/api/handler.go b/api/handler.go
index b7999d8..fd686c6 100644
--- a/api/handler.go
+++ b/api/handler.go
@@ -33,6 +33,7 @@ func Handler() *mux.Router {
 	m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies))
 	m.Get(router.SpeciesList).Handler(handler(serveSpeciesList))
 	m.Get(router.UpdateSpecies).Handler(handler(serveUpdateSpecies))
+	m.Get(router.DeleteSpecies).Handler(handler(serveDeleteSpecies))
 
 	return m
 }
diff --git a/api/species.go b/api/species.go
index 1e218a4..fe0b7c3 100644
--- a/api/species.go
+++ b/api/species.go
@@ -76,3 +76,17 @@ func serveUpdateSpecies(w http.ResponseWriter, r *http.Request) error {
 
 	return writeJSON(w, species)
 }
+
+func serveDeleteSpecies(w http.ResponseWriter, r *http.Request) error {
+	id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0)
+
+	deleted, err := store.Species.Delete(id)
+	if err != nil {
+		return err
+	}
+	if deleted {
+		w.WriteHeader(http.StatusOK)
+	}
+
+	return writeJSON(w, &models.Species{})
+}
diff --git a/api/species_test.go b/api/species_test.go
index a0c003a..418091e 100644
--- a/api/species_test.go
+++ b/api/species_test.go
@@ -118,3 +118,30 @@ func TestSpecies_Update(t *testing.T) {
 		t.Error("!success")
 	}
 }
+
+func TestSpecies_Delete(t *testing.T) {
+	setup()
+
+	want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	calledDelete := false
+	store.Species.(*models.MockSpeciesService).Delete_ = func(id int64) (bool, error) {
+		if id != want.Id {
+			t.Errorf("wanted request for species %d but got %d", want.Id, id)
+		}
+		calledDelete = true
+		return true, nil
+	}
+
+	success, err := apiClient.Species.Delete(1)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !calledDelete {
+		t.Error("!calledDelete")
+	}
+	if !success {
+		t.Error("!success")
+	}
+}
diff --git a/datastore/species.go b/datastore/species.go
index c3087c4..651c3e0 100644
--- a/datastore/species.go
+++ b/datastore/species.go
@@ -61,3 +61,19 @@ func (s *speciesStore) Update(id int64, species *models.Species) (bool, error) {
 
 	return true, nil
 }
+
+func (s *speciesStore) Delete(id int64) (bool, error) {
+	species, err := s.Get(id)
+	if err != nil {
+		return false, err
+	}
+
+	deleted, err := s.dbh.Delete(species)
+	if err != nil {
+		return false, err
+	}
+	if deleted == 0 {
+		return false, ErrNoRowsDeleted
+	}
+	return true, nil
+}
diff --git a/datastore/species_test.go b/datastore/species_test.go
index 1d7c3cd..5839e95 100644
--- a/datastore/species_test.go
+++ b/datastore/species_test.go
@@ -13,6 +13,7 @@ func TestSpeciesStore_Get_db(t *testing.T) {
 
 	// Test on a clean database
 	tx.Exec(`DELETE FROM species;`)
+	tx.Exec(`DELETE FROM genera;`)
 
 	wantGenus := &models.Genus{GenusName: "Test Genus"}
 	if err := tx.Insert(wantGenus); err != nil {
@@ -42,6 +43,7 @@ func TestSpeciesStore_Create_db(t *testing.T) {
 
 	// Test on a clean database
 	tx.Exec(`DELETE FROM species;`)
+	tx.Exec(`DELETE FROM genera;`)
 
 	genus := &models.Genus{}
 	if err := tx.Insert(genus); err != nil {
@@ -70,6 +72,7 @@ func TestSpeciesStore_List_db(t *testing.T) {
 
 	// Test on a clean database
 	tx.Exec(`DELETE FROM species;`)
+	tx.Exec(`DELETE FROM genera;`)
 
 	genus := &models.Genus{}
 
@@ -103,6 +106,7 @@ func TestSpeciesStore_Update_db(t *testing.T) {
 
 	// Test on a clean database
 	tx.Exec(`DELETE FROM species;`)
+	tx.Exec(`DELETE FROM genera;`)
 
 	d := NewDatastore(nil)
 	// Add a new record
@@ -131,3 +135,38 @@ func TestSpeciesStore_Update_db(t *testing.T) {
 		t.Error("!updated")
 	}
 }
+
+func TestSpeciesStore_Delete_db(t *testing.T) {
+	tx, _ := DB.Begin()
+	defer tx.Rollback()
+
+	// Test on a clean database
+	tx.Exec(`DELETE FROM species;`)
+	tx.Exec(`DELETE FROM genera;`)
+
+	d := NewDatastore(tx)
+	// Add a new record
+	genus := &models.Genus{GenusName: "Test Genus"}
+	_, err := d.Genera.Create(genus)
+	if err != nil {
+		t.Fatal(err)
+	}
+	species := &models.Species{GenusId: genus.Id, SpeciesName: "Test Species"}
+	created, err := d.Species.Create(species)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !created {
+		t.Error("!created")
+	}
+
+	// Delete it
+	deleted, err := d.Species.Delete(species.Id)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !deleted {
+		t.Error("!delete")
+	}
+}
diff --git a/models/species.go b/models/species.go
index b742de8..19b69fb 100644
--- a/models/species.go
+++ b/models/species.go
@@ -32,6 +32,9 @@ type SpeciesService interface {
 
 	// Update an existing species
 	Update(id int64, species *Species) (updated bool, err error)
+
+	// Delete an existing species
+	Delete(id int64) (deleted bool, err error)
 }
 
 var (
@@ -129,11 +132,34 @@ func (s *speciesService) Update(id int64, species *Species) (bool, error) {
 	return resp.StatusCode == http.StatusOK, nil
 }
 
+func (s *speciesService) Delete(id int64) (bool, error) {
+	strId := strconv.FormatInt(id, 10)
+
+	url, err := s.client.url(router.DeleteSpecies, map[string]string{"Id": strId}, nil)
+	if err != nil {
+		return false, err
+	}
+
+	req, err := s.client.NewRequest("DELETE", url.String(), nil)
+	if err != nil {
+		return false, err
+	}
+
+	var species *Species
+	resp, err := s.client.Do(req, &species)
+	if err != nil {
+		return false, err
+	}
+
+	return resp.StatusCode == http.StatusOK, nil
+}
+
 type MockSpeciesService struct {
 	Get_    func(id int64) (*Species, error)
 	List_   func(opt *SpeciesListOptions) ([]*Species, error)
 	Create_ func(species *Species) (bool, error)
 	Update_ func(id int64, species *Species) (bool, error)
+	Delete_ func(id int64) (bool, error)
 }
 
 var _ SpeciesService = &MockSpeciesService{}
@@ -165,3 +191,10 @@ func (s *MockSpeciesService) Update(id int64, species *Species) (bool, error) {
 	}
 	return s.Update_(id, species)
 }
+
+func (s *MockSpeciesService) Delete(id int64) (bool, error) {
+	if s.Delete_ == nil {
+		return false, nil
+	}
+	return s.Delete_(id)
+}
diff --git a/models/species_test.go b/models/species_test.go
index cb66f9e..f2ef1e9 100644
--- a/models/species_test.go
+++ b/models/species_test.go
@@ -136,3 +136,32 @@ func TestSpeciesService_Update(t *testing.T) {
 		t.Fatal("!called")
 	}
 }
+
+func TestSpeciesService_Delete(t *testing.T) {
+	setup()
+	defer teardown()
+
+	want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"}
+
+	var called bool
+	mux.HandleFunc(urlPath(t, router.DeleteSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) {
+		called = true
+		testMethod(t, r, "DELETE")
+
+		w.WriteHeader(http.StatusOK)
+		writeJSON(w, want)
+	})
+
+	deleted, err := client.Species.Delete(1)
+	if err != nil {
+		t.Errorf("Species.Delete returned error: %v", err)
+	}
+
+	if !deleted {
+		t.Error("!deleted")
+	}
+
+	if !called {
+		t.Fatal("!called")
+	}
+}
diff --git a/router/api.go b/router/api.go
index 5bf24d2..a7731ab 100644
--- a/router/api.go
+++ b/router/api.go
@@ -22,6 +22,7 @@ func API() *mux.Router {
 	m.Path("/species").Methods("POST").Name(CreateSpecies)
 	m.Path("/species/{Id:.+}").Methods("GET").Name(Species)
 	m.Path("/species/{Id:.+}").Methods("PUT").Name(UpdateSpecies)
+	m.Path("/species/{Id:.+}").Methods("DELETE").Name(DeleteSpecies)
 
 	return m
 }
diff --git a/router/routes.go b/router/routes.go
index 0db9205..124dd98 100644
--- a/router/routes.go
+++ b/router/routes.go
@@ -15,4 +15,5 @@ const (
 	CreateSpecies = "species:create"
 	SpeciesList   = "species:list"
 	UpdateSpecies = "species:update"
+	DeleteSpecies = "species:delete"
 )