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

#HofGenerator: gen.#HofGenerator & {
	// 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 (
	"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) {
	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.

2022 Hofstadter, Inc