API Resources



With our types and data store in place, we will now want to expose these in the API.

  • Add a Resource definition to the schema
  • Update our generator definition
  • Add the template for API resources
  • Wire resources into the router

Schema Additions

We first add a schema for a resource and a CUE ‘function’ for converting models to resources.

gen/server.cue

// Resources are pretty simple
#Resources: [string]: #Resource
#Resource: {
	Model:  #Model
	Name:   Model.Name
	Routes: #Routes
}

// We map from Datamodle to Resource
#DatamodelToResources: {
	Datamodel: #Datamodel
	Resources: #Resources & {
		for n, M in Datamodel.Models {
			// sub-value with same name as label as the model
			"\(n)": {
				// Same model and name
				Model: M
				Name:  M.Name

				// The default CRUD routes
				Routes: [{
					Name:   "\(M.Name)Create"
					Path:   ""
					Method: "POST"
				}, {
					Name: "\(M.Name)Read"
					Path: ""
					Params: ["\(strings.ToLower(M.Index))"]
					Method: "GET"
				}, {
					Name:   "\(M.Name)Update"
					Path:   ""
					Method: "PATCH"
				}, {
					Name: "\(M.Name)Delete"
					Path: ""
					Params: ["\(strings.ToLower(M.Index))"]
					Method: "DELETE"
				}, ...] // left open so you can add custom routes
			}
		}
	}
}

Generator Changes

Add the following changes in their appropriate places into the existing generator definition.

gen/server.cue

Generator: gen.Generator & {
	// We make resources from the data model
	// and there is no new inputs for the user
	In: {
		Resources: (schema.DatamodelToResources & {"Datamodel": Datamodel}).Resources
	}

	// Add a new line in _All
	All: [
		for _, F in ResourceFiles {F},
	]

	// Define the resource Files
	ResourceFiles: [...gen.File] & [
			for _, R in In.Resources {
			In: {
				RESOURCE: R
			}
			TemplatePath: "resource.go"
			Filepath:     "\(Outdir)/resources/\(In.RESOURCE.Name).go"
		},
	]

}

Resource Template

The following creates CRUD handlers. Note how we can reuse our route handler partial template because we added these in the schema and mapping.

Create a new template called resource.go

templates/resource.go

package resources

import (
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"
)

// {{ .RESOURCE.Name }}Routes sets up the routes in a router Group
func {{ .RESOURCE.Name }}Routes(G *echo.Group) {
	fmt.Println("adding {{.RESOURCE.Name}} routes")
	g := G.Group("/{{ kebab .RESOURCE.Name }}")

	// wire up CRUD routes
	{{ range $R := .RESOURCE.Routes }}
	g.{{ $R.Method }}( "{{ range $PATH := $R.Params }}/:{{$PATH}}{{ end }}", {{ $R.Name }}Handler)
	{{- end }}
}

{{ range $R := .RESOURCE.Routes }}
{{ template "handler.go" $R }}
{{ end }}

Other Templates

Some small changes to existing templates as well

templates/router.go

import (
	// ...
	
	{{ if gt (len .Resources ) 1 }}
	"{{ .Module }}/resources"
	{{ end }}
)

func setupRouter(e *echo.Echo) error {
	// ...
	{{ range $R := .Resources -}}
	resources.{{ $R.Name }}Routes(g)
	{{ end }}

	return nil
}

Regenerate the Server

You can now run hof gen ./example and you should find a new ./output/resources directory.

Using the Resources

You could now rebuild and call our CRUD endpoints, except that we haven’t yet implemented the handlers, which we will do in the next section.