func main() { index := resource.NewIndex() users := index.Bind("users", resource.New(models.UserSchema, mem.NewHandler(), resource.Conf{ AllowedModes: resource.ReadWrite, PaginationDefaultLimit: 50, })) models.SetAuthUserResource(users) index.Bind("auth", resource.New(models.AuthSchema, mem.NewHandler(), resource.Conf{ AllowedModes: []resource.Mode{resource.Create}, })) api, err := rest.NewHandler(index) if err != nil { log.Fatal(err) } http.Handle("/", api) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
func main() { // Create a REST API resource index index := resource.NewIndex() // Add a resource on /users[/:user_id] users := index.Bind("users", resource.New(user, mem.NewHandler(), resource.Conf{ // We allow all REST methods // (rest.ReadWrite is a shortcut for []resource.Mode{resource.Create, resource.Read, resource.Update, resource.Delete, resource,List}) AllowedModes: resource.ReadWrite, })) // Bind a sub resource on /users/:user_id/posts[/:post_id] // and reference the user on each post using the "user" field of the posts resource. posts := users.Bind("posts", "user", resource.New(post, mem.NewHandler(), resource.Conf{ // Posts can only be read, created and deleted, not updated AllowedModes: []resource.Mode{resource.Read, resource.List, resource.Create, resource.Delete}, })) // Add a friendly alias to public posts // (equivalent to /users/:user_id/posts?filter={"published":true}) posts.Alias("public", url.Values{"filter": []string{"{\"published\":true}"}}) // Create API HTTP handler for the resource graph api, err := rest.NewHandler(index) if err != nil { log.Fatalf("Invalid API configuration: %s", err) } // Init a xhandler chain (see https://github.com/rs/xhandler) c := xhandler.Chain{} // Add close notifier handler so context is cancelled when the client closes // the connection c.UseC(xhandler.CloseHandler) // Add timeout handler c.UseC(xhandler.TimeoutHandler(2 * time.Second)) // Install a logger (see https://github.com/rs/xlog) c.UseC(xlog.NewHandler(xlog.Config{})) // Log API access c.UseC(xaccess.NewHandler()) // Add CORS support with passthrough option on so rest-layer can still // handle OPTIONS method c.UseC(cors.New(cors.Options{OptionsPassthrough: true}).HandlerC) // Bind the API under /api/ path http.Handle("/api/", http.StripPrefix("/api/", c.Handler(api))) // Serve it log.Print("Serving API on http://localhost:8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
func TestHandlerServeHTTPParentNotFound(t *testing.T) { i := resource.NewIndex() foo := i.Bind("foo", resource.New(schema.Schema{}, mem.NewHandler(), resource.DefaultConf)) foo.Bind("bar", "f", resource.New(schema.Schema{"f": schema.Field{}}, nil, resource.DefaultConf)) h, _ := NewHandler(i) w := newRecorder() defer w.Close() u, _ := url.ParseRequestURI("/foo/1/bar/2") h.ServeHTTP(w, &http.Request{Method: "GET", URL: u}) assert.Equal(t, 404, w.Code) b, _ := ioutil.ReadAll(w.Body) assert.Equal(t, "{\"code\":404,\"message\":\"Parent Resource Not Found\"}", string(b)) }
func main() { // Create a REST API resource index index := resource.NewIndex() // Bind user on /users users := index.Bind("users", resource.New(user, mem.NewHandler(), resource.Conf{ AllowedModes: resource.ReadWrite, })) // Init the db with some users (user registration is not handled by this example) secret, _ := schema.Password{}.Validate("secret") users.Insert(context.Background(), []*resource.Item{ &resource.Item{ID: "admin", Updated: time.Now(), ETag: "abcd", Payload: map[string]interface{}{ "id": "admin", "name": "Dilbert", "password": secret, }}, &resource.Item{ID: "john", Updated: time.Now(), ETag: "efgh", Payload: map[string]interface{}{ "id": "john", "name": "John Doe", "password": secret, }}, }) // Bind post on /posts index.Bind("posts", resource.New(post, mem.NewHandler(), resource.Conf{ AllowedModes: resource.ReadWrite, })) // Create API HTTP handler for the resource graph api, err := rest.NewHandler(index) if err != nil { log.Fatalf("Invalid API configuration: %s", err) } // Bind the authentication middleware // api.Use(myAuthMiddleware{userResource: users}) // Bind the API under / http.Handle("/", api) // Serve it log.Print("Serving API on http://localhost:8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
func TestRoutePathParentsNotExists(t *testing.T) { index := resource.NewIndex() i, _ := resource.NewItem(map[string]interface{}{"id": "1234"}) h := &mockHandler{[]*resource.Item{i}, nil, []schema.Query{}} empty := &mockHandler{[]*resource.Item{}, nil, []schema.Query{}} foo := index.Bind("foo", resource.New(schema.Schema{}, empty, resource.DefaultConf)) foo.Bind("bar", "f", resource.New(schema.Schema{"f": schema.Field{}}, h, resource.DefaultConf)) ctx := context.Background() route := newRoute("GET") // non existing foo err := findRoute("/foo/4321/bar/1234", index, route) if assert.NoError(t, err) { err := route.ResourcePath.ParentsExist(ctx) assert.Equal(t, &Error{404, "Parent Resource Not Found", nil}, err) } }
func TestRoutePathParentsExists(t *testing.T) { var route *RouteMatch var err error index := resource.NewIndex() i, _ := resource.NewItem(map[string]interface{}{"id": "1234"}) h := &mockHandler{[]*resource.Item{i}, nil, []schema.Query{}} foo := index.Bind("foo", resource.New(schema.Schema{}, h, resource.DefaultConf)) bar := foo.Bind("bar", "f", resource.New(schema.Schema{"f": schema.Field{}}, h, resource.DefaultConf)) bar.Bind("baz", "b", resource.New(schema.Schema{"f": schema.Field{}, "b": schema.Field{}}, h, resource.DefaultConf)) ctx := context.Background() route = newRoute("GET") err = findRoute("/foo/1234/bar/5678/baz/9000", index, route) if assert.NoError(t, err) { err = route.ResourcePath.ParentsExist(ctx) assert.NoError(t, err) // There's 3 components in the path but only 2 are parents assert.Len(t, h.queries, 2) // query on /foo/1234 assert.Contains(t, h.queries, schema.Query{schema.Equal{Field: "id", Value: "1234"}}) // query on /bar/5678 with foo/1234 context assert.Contains(t, h.queries, schema.Query{schema.Equal{Field: "f", Value: "1234"}, schema.Equal{Field: "id", Value: "5678"}}) } route = newRoute("GET") // empty the storage handler h.items = []*resource.Item{} err = findRoute("/foo/1234/bar", index, route) if assert.NoError(t, err) { err = route.ResourcePath.ParentsExist(ctx) assert.Equal(t, &Error{404, "Parent Resource Not Found", nil}, err) } route = newRoute("GET") // for error h.err = errors.New("test") err = findRoute("/foo/1234/bar", index, route) if assert.NoError(t, err) { err = route.ResourcePath.ParentsExist(ctx) assert.EqualError(t, err, "test") } }
func TestHandlerServeHTTP(t *testing.T) { i := resource.NewIndex() i.Bind("foo", resource.New(schema.Schema{}, nil, resource.DefaultConf)) h, _ := NewHandler(i) w := newRecorder() defer w.Close() u, _ := url.ParseRequestURI("/foo") h.ServeHTTP(w, &http.Request{Method: "GET", URL: u}) assert.Equal(t, 501, w.Code) b, _ := ioutil.ReadAll(w.Body) assert.Equal(t, "{\"code\":501,\"message\":\"No Storage Defined\"}", string(b)) }
func TestNewHandlerNoCompile(t *testing.T) { i := resource.NewIndex() i.Bind("foo", resource.New(schema.Schema{ "f": schema.Field{ Validator: schema.String{ Regexp: "[", }, }, }, nil, resource.DefaultConf)) _, err := NewHandler(i) assert.EqualError(t, err, "foo: schema compilation error: f: not a schema.Validator pointer") }
func main() { // Create a REST API resource index index := resource.NewIndex() // Add a resource on /users[/:user_id] users := index.Bind("users", resource.New(user, mem.NewHandler(), resource.Conf{ // We allow all REST methods // (rest.ReadWrite is a shortcut for []resource.Mode{resource.Create, resource.Read, resource.Update, resource.Delete, resource,List}) AllowedModes: resource.ReadWrite, })) // Bind a sub resource on /users/:user_id/posts[/:post_id] // and reference the user on each post using the "user" field of the posts resource. posts := users.Bind("posts", "user", resource.New(post, mem.NewHandler(), resource.Conf{ // Posts can only be read, created and deleted, not updated AllowedModes: []resource.Mode{resource.Read, resource.List, resource.Create, resource.Delete}, })) // Add a friendly alias to public posts // (equivalent to /users/:user_id/posts?filter={"published":true}) posts.Alias("public", url.Values{"filter": []string{"{\"published\":true}"}}) // Create API HTTP handler for the resource graph api, err := rest.NewHandler(index) if err != nil { log.Fatalf("Invalid API configuration: %s", err) } // Add cors support h := cors.New(cors.Options{OptionsPassthrough: true}).Handler(api) // Bind the API under /api/ path http.Handle("/api/", http.StripPrefix("/api/", h)) // Serve it log.Print("Serving API on http://localhost:8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
func main() { index := resource.NewIndex() index.Bind("posts", resource.New(post, mem.NewHandler(), resource.Conf{ // Posts can only be read, created and deleted, not updated AllowedModes: resource.ReadWrite, })) // Create API HTTP handler for the resource graph api, err := rest.NewHandler(index) if err != nil { log.Fatalf("Invalid API configuration: %s", err) } // Bind the API under /api/ path http.Handle("/", api) // Serve it log.Print("Serving API on http://localhost:8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
func TestFindRoute(t *testing.T) { var route *RouteMatch var err *Error index := resource.NewIndex() i, _ := resource.NewItem(map[string]interface{}{"id": "1234"}) h := &mockHandler{[]*resource.Item{i}, nil, []schema.Query{}} foo := index.Bind("foo", resource.New(schema.Schema{}, h, resource.DefaultConf)) bar := foo.Bind("bar", "f", resource.New(schema.Schema{"f": schema.Field{}}, h, resource.DefaultConf)) barbar := bar.Bind("bar", "b", resource.New(schema.Schema{"b": schema.Field{}}, h, resource.DefaultConf)) bar.Alias("baz", url.Values{"sort": []string{"foo"}}) route = newRoute("GET") err = findRoute("/foo", index, route) if assert.Nil(t, err) { assert.Equal(t, foo, route.Resource()) assert.Equal(t, url.Values{}, route.Params) assert.Nil(t, route.ResourceID()) rp := route.ResourcePath if assert.Len(t, rp, 1) { assert.Equal(t, "foo", rp[0].Name) assert.Equal(t, "", rp[0].Field) assert.Nil(t, rp[0].Value) } } route = newRoute("GET") err = findRoute("/foo/1234", index, route) if assert.Nil(t, err) { assert.Equal(t, foo, route.Resource()) assert.Equal(t, url.Values{}, route.Params) assert.Equal(t, "1234", route.ResourceID()) rp := route.ResourcePath if assert.Len(t, rp, 1) { assert.Equal(t, "foo", rp[0].Name) assert.Equal(t, "id", rp[0].Field) assert.Equal(t, "1234", rp[0].Value) } } route = newRoute("GET") err = findRoute("/foo/1234/bar", index, route) if assert.Nil(t, err) { assert.Equal(t, bar, route.Resource()) assert.Nil(t, route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) rp := route.ResourcePath if assert.Len(t, rp, 2) { assert.Equal(t, "foo", rp[0].Name) assert.Equal(t, "f", rp[0].Field) assert.Equal(t, "1234", rp[0].Value) assert.Equal(t, "bar", rp[1].Name) assert.Equal(t, "", rp[1].Field) assert.Nil(t, rp[1].Value) } } route = newRoute("GET") err = findRoute("/foo/1234/bar/1234", index, route) if assert.Nil(t, err) { assert.Equal(t, bar, route.Resource()) assert.Equal(t, "1234", route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) rp := route.ResourcePath if assert.Len(t, rp, 2) { assert.Equal(t, "foo", rp[0].Name) assert.Equal(t, "f", rp[0].Field) assert.Equal(t, "1234", rp[0].Value) assert.Equal(t, "bar", rp[1].Name) assert.Equal(t, "id", rp[1].Field) assert.Equal(t, "1234", rp[1].Value) } } route = newRoute("GET") err = findRoute("/foo/1234/bar/1234/bar", index, route) if assert.Nil(t, err) { assert.Equal(t, barbar, route.Resource()) assert.Nil(t, route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) rp := route.ResourcePath if assert.Len(t, rp, 3) { assert.Equal(t, "foo", rp[0].Name) assert.Equal(t, "f", rp[0].Field) assert.Equal(t, "1234", rp[0].Value) assert.Equal(t, "bar", rp[1].Name) assert.Equal(t, "b", rp[1].Field) assert.Equal(t, "1234", rp[1].Value) assert.Equal(t, "bar", rp[2].Name) assert.Equal(t, "", rp[2].Field) assert.Nil(t, rp[2].Value) } } route = newRoute("GET") err = findRoute("/foo/1234/bar/1234/bar/1234", index, route) if assert.Nil(t, err) { assert.Equal(t, barbar, route.Resource()) assert.Equal(t, "1234", route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) rp := route.ResourcePath if assert.Len(t, rp, 3) { assert.Equal(t, "foo", rp[0].Name) assert.Equal(t, "f", rp[0].Field) assert.Equal(t, "1234", rp[0].Value) assert.Equal(t, "bar", rp[1].Name) assert.Equal(t, "b", rp[1].Field) assert.Equal(t, "1234", rp[1].Value) assert.Equal(t, "bar", rp[2].Name) assert.Equal(t, "id", rp[2].Field) assert.Equal(t, "1234", rp[2].Value) } } route = newRoute("GET") err = findRoute("/foo/1234/bar/baz", index, route) if assert.Nil(t, err) { assert.Equal(t, bar, route.Resource()) assert.Equal(t, url.Values{"sort": []string{"foo"}}, route.Params) assert.Nil(t, route.ResourceID()) rp := route.ResourcePath if assert.Len(t, rp, 2) { assert.Equal(t, "foo", rp[0].Name) assert.Equal(t, "f", rp[0].Field) assert.Equal(t, "1234", rp[0].Value) assert.Equal(t, "bar", rp[1].Name) assert.Equal(t, "", rp[1].Field) assert.Nil(t, rp[1].Value) } } route = newRoute("GET") err = findRoute("/foo/1234/bar/baz/baz", index, route) assert.Equal(t, &Error{404, "Resource Not Found", nil}, err) assert.Nil(t, route.Resource()) assert.Nil(t, route.ResourceID()) }
func TestFindRoute(t *testing.T) { var route RouteMatch var err *Error index := resource.NewIndex() i, _ := resource.NewItem(map[string]interface{}{"id": "1234"}) h := &mockHandler{[]*resource.Item{i}, nil} foo := index.Bind("foo", resource.New(schema.Schema{}, h, resource.DefaultConf)) bar := foo.Bind("bar", "f", resource.New(schema.Schema{"f": schema.Field{}}, h, resource.DefaultConf)) barbar := bar.Bind("bar", "b", resource.New(schema.Schema{"b": schema.Field{}}, h, resource.DefaultConf)) bar.Alias("baz", url.Values{"sort": []string{"foo"}}) ctx := context.Background() route = newRoute("GET") err = findRoute(ctx, "/foo", index, &route) if assert.Nil(t, err) { assert.Equal(t, foo, route.Resource()) assert.Equal(t, url.Values{}, route.Params) assert.Nil(t, route.ResourceID()) if assert.Len(t, route.ResourcePath, 1) { assert.Equal(t, "foo", route.ResourcePath[0].Name) assert.Equal(t, "", route.ResourcePath[0].Field) assert.Nil(t, route.ResourcePath[0].Value) } } route = newRoute("GET") err = findRoute(ctx, "/foo/1234", index, &route) if assert.Nil(t, err) { assert.Equal(t, foo, route.Resource()) assert.Equal(t, url.Values{}, route.Params) assert.Equal(t, "1234", route.ResourceID()) if assert.Len(t, route.ResourcePath, 1) { assert.Equal(t, "foo", route.ResourcePath[0].Name) assert.Equal(t, "id", route.ResourcePath[0].Field) assert.Equal(t, "1234", route.ResourcePath[0].Value) } } route = newRoute("GET") err = findRoute(ctx, "/foo/1234/bar", index, &route) if assert.Nil(t, err) { assert.Equal(t, bar, route.Resource()) assert.Nil(t, route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) if assert.Len(t, route.ResourcePath, 2) { assert.Equal(t, "foo", route.ResourcePath[0].Name) assert.Equal(t, "f", route.ResourcePath[0].Field) assert.Equal(t, "1234", route.ResourcePath[0].Value) assert.Equal(t, "bar", route.ResourcePath[1].Name) assert.Equal(t, "", route.ResourcePath[1].Field) assert.Nil(t, route.ResourcePath[1].Value) } } route = newRoute("GET") err = findRoute(ctx, "/foo/1234/bar/1234", index, &route) if assert.Nil(t, err) { assert.Equal(t, bar, route.Resource()) assert.Equal(t, "1234", route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) if assert.Len(t, route.ResourcePath, 2) { assert.Equal(t, "foo", route.ResourcePath[0].Name) assert.Equal(t, "f", route.ResourcePath[0].Field) assert.Equal(t, "1234", route.ResourcePath[0].Value) assert.Equal(t, "bar", route.ResourcePath[1].Name) assert.Equal(t, "id", route.ResourcePath[1].Field) assert.Equal(t, "1234", route.ResourcePath[1].Value) } } route = newRoute("GET") err = findRoute(ctx, "/foo/1234/bar/1234/bar", index, &route) if assert.Nil(t, err) { assert.Equal(t, barbar, route.Resource()) assert.Nil(t, route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) if assert.Len(t, route.ResourcePath, 3) { assert.Equal(t, "foo", route.ResourcePath[0].Name) assert.Equal(t, "f", route.ResourcePath[0].Field) assert.Equal(t, "1234", route.ResourcePath[0].Value) assert.Equal(t, "bar", route.ResourcePath[1].Name) assert.Equal(t, "b", route.ResourcePath[1].Field) assert.Equal(t, "1234", route.ResourcePath[1].Value) assert.Equal(t, "bar", route.ResourcePath[2].Name) assert.Equal(t, "", route.ResourcePath[2].Field) assert.Nil(t, route.ResourcePath[2].Value) } } route = newRoute("GET") err = findRoute(ctx, "/foo/1234/bar/1234/bar/1234", index, &route) if assert.Nil(t, err) { assert.Equal(t, barbar, route.Resource()) assert.Equal(t, "1234", route.ResourceID()) assert.Equal(t, url.Values{}, route.Params) if assert.Len(t, route.ResourcePath, 3) { assert.Equal(t, "foo", route.ResourcePath[0].Name) assert.Equal(t, "f", route.ResourcePath[0].Field) assert.Equal(t, "1234", route.ResourcePath[0].Value) assert.Equal(t, "bar", route.ResourcePath[1].Name) assert.Equal(t, "b", route.ResourcePath[1].Field) assert.Equal(t, "1234", route.ResourcePath[1].Value) assert.Equal(t, "bar", route.ResourcePath[2].Name) assert.Equal(t, "id", route.ResourcePath[2].Field) assert.Equal(t, "1234", route.ResourcePath[2].Value) } } route = newRoute("GET") err = findRoute(ctx, "/foo/1234/bar/baz", index, &route) if assert.Nil(t, err) { assert.Equal(t, bar, route.Resource()) assert.Equal(t, url.Values{"sort": []string{"foo"}}, route.Params) assert.Nil(t, route.ResourceID()) if assert.Len(t, route.ResourcePath, 2) { assert.Equal(t, "foo", route.ResourcePath[0].Name) assert.Equal(t, "f", route.ResourcePath[0].Field) assert.Equal(t, "1234", route.ResourcePath[0].Value) assert.Equal(t, "bar", route.ResourcePath[1].Name) assert.Equal(t, "", route.ResourcePath[1].Field) assert.Nil(t, route.ResourcePath[1].Value) } } route = newRoute("GET") err = findRoute(ctx, "/foo/1234/bar/baz/baz", index, &route) assert.Equal(t, &Error{404, "Resource Not Found", nil}, err) assert.Nil(t, route.Resource()) assert.Nil(t, route.ResourceID()) route = newRoute("GET") // empty the storage handler h.items = []*resource.Item{} err = findRoute(ctx, "/foo/1234/bar", index, &route) assert.Equal(t, ErrNotFound, err) route = newRoute("GET") // for error h.err = errors.New("test") err = findRoute(ctx, "/foo/1234/bar", index, &route) assert.Equal(t, &Error{520, "test", nil}, err) }
func Example() { var ( // Define a user resource schema user = schema.Schema{ "id": schema.Field{ Required: true, // When a field is read-only, on default values or hooks can // set their value. The client can't change it. ReadOnly: true, // This is a field hook called when a new user is created. // The schema.NewID hook is a provided hook to generate a // unique id when no value is provided. OnInit: &schema.NewID, // The Filterable and Sortable allows usage of filter and sort // on this field in requests. Filterable: true, Sortable: true, Validator: &schema.String{ Regexp: "^[0-9a-f]{32}$", }, }, "created": schema.Field{ Required: true, ReadOnly: true, Filterable: true, Sortable: true, OnInit: &schema.Now, Validator: &schema.Time{}, }, "updated": schema.Field{ Required: true, ReadOnly: true, Filterable: true, Sortable: true, OnInit: &schema.Now, // The OnUpdate hook is called when the item is edited. Here we use // provided Now hook which just return the current time. OnUpdate: &schema.Now, Validator: &schema.Time{}, }, // Define a name field as required with a string validator "name": schema.Field{ Required: true, Filterable: true, Validator: &schema.String{ MaxLen: 150, }, }, } // Define a post resource schema post = schema.Schema{ // schema.*Field are shortcuts for common fields (identical to users' same fields) "id": schema.IDField, "created": schema.CreatedField, "updated": schema.UpdatedField, // Define a user field which references the user owning the post. // See bellow, the content of this field is enforced by the fact // that posts is a sub-resource of users. "user": schema.Field{ Required: true, Filterable: true, Validator: &schema.Reference{ Path: "users", }, }, "public": schema.Field{ Filterable: true, Validator: &schema.Bool{}, }, // Sub-documents are handled via a sub-schema "meta": schema.Field{ Schema: &schema.Schema{ "title": schema.Field{ Required: true, Validator: &schema.String{ MaxLen: 150, }, }, "body": schema.Field{ Validator: &schema.String{ MaxLen: 100000, }, }, }, }, } ) // Create a REST API root resource index := resource.NewIndex() // Add a resource on /users[/:user_id] users := index.Bind("users", resource.New(user, mem.NewHandler(), resource.Conf{ // We allow all REST methods // (rest.ReadWrite is a shortcut for []rest.Mode{Create, Read, Update, Delete, List}) AllowedModes: resource.ReadWrite, })) // Bind a sub resource on /users/:user_id/posts[/:post_id] // and reference the user on each post using the "user" field of the posts resource. posts := users.Bind("posts", "user", resource.New(post, mem.NewHandler(), resource.Conf{ // Posts can only be read, created and deleted, not updated AllowedModes: []resource.Mode{resource.Read, resource.List, resource.Create, resource.Delete}, })) // Add a friendly alias to public posts // (equivalent to /users/:user_id/posts?filter={"public":true}) posts.Alias("public", url.Values{"filter": []string{"{\"public\"=true}"}}) // Create API HTTP handler for the resource graph api, err := rest.NewHandler(index) if err != nil { log.Fatalf("Invalid API configuration: %s", err) } // Add cors support h := cors.New(cors.Options{OptionsPassthrough: true}).Handler(api) // Bind the API under /api/ path http.Handle("/api/", http.StripPrefix("/api/", h)) // Serve it log.Print("Serving API on http://localhost:8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }