GA Logo

Full Stack Build Part 1 - Building and Deploying an API


What you will learn

  • Creating an API
  • Setting Cors Headers
  • Testing an API
  • Deploying an API

Setup

  • start a new project following the same setup steps as yesterday mornings lesson buffalo new gotodosapi --api
  • enter database details in your database.yml and create your database buffalo pop create -e development
  • Run your dev server

Creating the API

In Buffalo, we can simplify a lot of what we did yesterday by generating a resource which will generate the model, migrations, actions and routes we need to build our api. Just starting with the following command.

buffalo generate resource todo subject:string details:string

Nice, we can even specify the fields in the command! Let's make one slight modification to our migrations and model and change the ID type from UUID to int.

The Migration file in the migration folder...

create_table("todoes") {
	t.Column("id", "int", {primary: true})
	t.Column("subject", "string", {})
	t.Column("details", "string", {})
	t.Timestamps()
}

The model in the models folder

package models

import (
	"encoding/json"
	"github.com/gobuffalo/pop/v5"
	"github.com/gobuffalo/validate/v3"
	// "github.com/gofrs/uuid"
	"time"
	"github.com/gobuffalo/validate/v3/validators"
)
// Todo is used by pop to map your todoes database table to your go code.
type Todo struct {
    ID int `json:"id" db:"id"`
    Subject string `json:"subject" db:"subject"`
    Details string `json:"details" db:"details"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
    UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}

// String is not required by pop and may be deleted
func (t Todo) String() string {
	jt, _ := json.Marshal(t)
	return string(jt)
}

// Todoes is not required by pop and may be deleted
type Todoes []Todo

// String is not required by pop and may be deleted
func (t Todoes) String() string {
	jt, _ := json.Marshal(t)
	return string(jt)
}

// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
// This method is not required and may be deleted.
func (t *Todo) Validate(tx *pop.Connection) (*validate.Errors, error) {
	return validate.Validate(
		&validators.StringIsPresent{Field: t.Subject, Name: "Subject"},
		&validators.StringIsPresent{Field: t.Details, Name: "Details"},
	), nil
}

// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method.
// This method is not required and may be deleted.
func (t *Todo) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) {
	return validate.NewErrors(), nil
}

// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method.
// This method is not required and may be deleted.
func (t *Todo) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) {
	return validate.NewErrors(), nil
}
  • Migrate the database with command buffalo pop migrate
  • You'll notice all the resource methods have already been completed with a robust boilerplate in actions/todoes.go.
package actions

import (

  "fmt"
  "net/http"
  "github.com/gobuffalo/buffalo"
  "github.com/gobuffalo/pop/v5"
  "github.com/gobuffalo/x/responder"
  "gotodosapi/models"
)

// This file is generated by Buffalo. It offers a basic structure for
// adding, editing and deleting a page. If your model is more
// complex or you need more than the basic implementation you need to
// edit this file.

// Following naming logic is implemented in Buffalo:
// Model: Singular (Todo)
// DB Table: Plural (todoes)
// Resource: Plural (Todoes)
// Path: Plural (/todoes)
// View Template Folder: Plural (/templates/todoes/)

// TodoesResource is the resource for the Todo model
type TodoesResource struct{
  buffalo.Resource
}

// List gets all Todoes. This function is mapped to the path
// GET /todoes
func (v TodoesResource) List(c buffalo.Context) error {
  // Get the DB connection from the context
  tx, ok := c.Value("tx").(*pop.Connection)
  if !ok {
    return fmt.Errorf("no transaction found")
  }

  todoes := &models.Todoes{}

  // Paginate results. Params "page" and "per_page" control pagination.
  // Default values are "page=1" and "per_page=20".
  q := tx.PaginateFromParams(c.Params())

  // Retrieve all Todoes from the DB
  if err := q.All(todoes); err != nil {
    return err
  }

  return responder.Wants("html", func (c buffalo.Context) error {
    // Add the paginator to the context so it can be used in the template.
    c.Set("pagination", q.Paginator)

    c.Set("todoes", todoes)
    return c.Render(http.StatusOK, r.HTML("/todoes/index.plush.html"))
  }).Wants("json", func (c buffalo.Context) error {
    return c.Render(200, r.JSON(todoes))
  }).Wants("xml", func (c buffalo.Context) error {
    return c.Render(200, r.XML(todoes))
  }).Respond(c)
}

// Show gets the data for one Todo. This function is mapped to
// the path GET /todoes/{todo_id}
func (v TodoesResource) Show(c buffalo.Context) error {
  // Get the DB connection from the context
  tx, ok := c.Value("tx").(*pop.Connection)
  if !ok {
    return fmt.Errorf("no transaction found")
  }

  // Allocate an empty Todo
  todo := &models.Todo{}

  // To find the Todo the parameter todo_id is used.
  if err := tx.Find(todo, c.Param("todo_id")); err != nil {
    return c.Error(http.StatusNotFound, err)
  }

  return responder.Wants("html", func (c buffalo.Context) error {
    c.Set("todo", todo)

    return c.Render(http.StatusOK, r.HTML("/todoes/show.plush.html"))
  }).Wants("json", func (c buffalo.Context) error {
    return c.Render(200, r.JSON(todo))
  }).Wants("xml", func (c buffalo.Context) error {
    return c.Render(200, r.XML(todo))
  }).Respond(c)
}

// Create adds a Todo to the DB. This function is mapped to the
// path POST /todoes
func (v TodoesResource) Create(c buffalo.Context) error {
  // Allocate an empty Todo
  todo := &models.Todo{}

  // Bind todo to the html form elements
  if err := c.Bind(todo); err != nil {
    return err
  }

  // Get the DB connection from the context
  tx, ok := c.Value("tx").(*pop.Connection)
  if !ok {
    return fmt.Errorf("no transaction found")
  }

  // Validate the data from the html form
  verrs, err := tx.ValidateAndCreate(todo)
  if err != nil {
    return err
  }

  if verrs.HasAny() {
    return responder.Wants("html", func (c buffalo.Context) error {
      // Make the errors available inside the html template
      c.Set("errors", verrs)

      // Render again the new.html template that the user can
      // correct the input.
      c.Set("todo", todo)

      return c.Render(http.StatusUnprocessableEntity, r.HTML("/todoes/new.plush.html"))
    }).Wants("json", func (c buffalo.Context) error {
      return c.Render(http.StatusUnprocessableEntity, r.JSON(verrs))
    }).Wants("xml", func (c buffalo.Context) error {
      return c.Render(http.StatusUnprocessableEntity, r.XML(verrs))
    }).Respond(c)
  }

  return responder.Wants("html", func (c buffalo.Context) error {
    // If there are no errors set a success message
    c.Flash().Add("success", T.Translate(c, "todo.created.success"))

    // and redirect to the show page
    return c.Redirect(http.StatusSeeOther, "/todoes/%v", todo.ID)
  }).Wants("json", func (c buffalo.Context) error {
    return c.Render(http.StatusCreated, r.JSON(todo))
  }).Wants("xml", func (c buffalo.Context) error {
    return c.Render(http.StatusCreated, r.XML(todo))
  }).Respond(c)
}

// Update changes a Todo in the DB. This function is mapped to
// the path PUT /todoes/{todo_id}
func (v TodoesResource) Update(c buffalo.Context) error {
  // Get the DB connection from the context
  tx, ok := c.Value("tx").(*pop.Connection)
  if !ok {
    return fmt.Errorf("no transaction found")
  }

  // Allocate an empty Todo
  todo := &models.Todo{}

  if err := tx.Find(todo, c.Param("todo_id")); err != nil {
    return c.Error(http.StatusNotFound, err)
  }

  // Bind Todo to the html form elements
  if err := c.Bind(todo); err != nil {
    return err
  }

  verrs, err := tx.ValidateAndUpdate(todo)
  if err != nil {
    return err
  }

  if verrs.HasAny() {
    return responder.Wants("html", func (c buffalo.Context) error {
      // Make the errors available inside the html template
      c.Set("errors", verrs)

      // Render again the edit.html template that the user can
      // correct the input.
      c.Set("todo", todo)

      return c.Render(http.StatusUnprocessableEntity, r.HTML("/todoes/edit.plush.html"))
    }).Wants("json", func (c buffalo.Context) error {
      return c.Render(http.StatusUnprocessableEntity, r.JSON(verrs))
    }).Wants("xml", func (c buffalo.Context) error {
      return c.Render(http.StatusUnprocessableEntity, r.XML(verrs))
    }).Respond(c)
  }

  return responder.Wants("html", func (c buffalo.Context) error {
    // If there are no errors set a success message
    c.Flash().Add("success", T.Translate(c, "todo.updated.success"))

    // and redirect to the show page
    return c.Redirect(http.StatusSeeOther, "/todoes/%v", todo.ID)
  }).Wants("json", func (c buffalo.Context) error {
    return c.Render(http.StatusOK, r.JSON(todo))
  }).Wants("xml", func (c buffalo.Context) error {
    return c.Render(http.StatusOK, r.XML(todo))
  }).Respond(c)
}

// Destroy deletes a Todo from the DB. This function is mapped
// to the path DELETE /todoes/{todo_id}
func (v TodoesResource) Destroy(c buffalo.Context) error {
  // Get the DB connection from the context
  tx, ok := c.Value("tx").(*pop.Connection)
  if !ok {
    return fmt.Errorf("no transaction found")
  }

  // Allocate an empty Todo
  todo := &models.Todo{}

  // To find the Todo the parameter todo_id is used.
  if err := tx.Find(todo, c.Param("todo_id")); err != nil {
    return c.Error(http.StatusNotFound, err)
  }

  if err := tx.Destroy(todo); err != nil {
    return err
  }

  return responder.Wants("html", func (c buffalo.Context) error {
    // If there are no errors set a flash message
    c.Flash().Add("success", T.Translate(c, "todo.destroyed.success"))

    // Redirect to the index page
    return c.Redirect(http.StatusSeeOther, "/todoes")
  }).Wants("json", func (c buffalo.Context) error {
    return c.Render(http.StatusOK, r.JSON(todo))
  }).Wants("xml", func (c buffalo.Context) error {
    return c.Render(http.StatusOK, r.XML(todo))
  }).Respond(c)
}
  • update the routes entry in app.go
	app.Resource("/todos", TodoesResource{})

Testing the API

Now let's test the API!

  • Make a post request to /todos to create a todo
  • Make a get request to /todos to see the list of todos
  • make a get request to /todos/1 to see one todo alone
  • update the todo with a put request to /todos/1
  • delete the todo with a delete request to /todo/1

    Deploying the API

  • update the following portion of app.go to enable and customize cors access
func App() *buffalo.App {
	if app == nil {

		customCors := cors.New(cors.Options{
			AllowedOrigins: []string{"*"},
			AllowedMethods: []string{"*"},
			AllowedHeaders: []string{"*"},
			AllowCredentials: true,
			// Enable Debugging for testing, consider disabling in production
			Debug: true,
		})

		app = buffalo.New(buffalo.Options{
			Env:          ENV,
			SessionStore: sessions.Null{},
			PreWares: []buffalo.PreWare{
				// cors.Default().Handler,
				customCors.Handler,
			},
			SessionName: "_gotodosapi_session",
		})
  • Add this comment in your go.mod with your version of go // +heroku goVersion 1.16
  • create a Procfile in the root of your project with the following
web: ./bin/gotodosapi
  • create a new project on Heroku, under the resources tab and create a new Heroku Postgres database
  • head over to settings and add the following config var
GO_ENV=production
  • go back to resources click on the database to go to its dashboard, go to settings and find the connection URI under credentials.
  • copy it to your local .env in your project
DATABASE_URL=postgres://username:password@server.amazonaws.com:5432/databasename
  • migrate your database with the command buffalo pop migrate -e production
  • commit and push your code to GitHub
  • connect it to your Heroku project under the deploy tab, enable automatic deploys, and trigger a manual deploy

Resources to Learn More