Skip to Content
DocumentationGuidesREST API

REST API

REST is the default API mode in Gofasta. When you run gofasta new myapp, the generated project includes a fully working REST API with no additional flags required. GraphQL can be added optionally via the --graphql flag.

This guide covers how controllers, routes, DTOs, middleware, and response helpers work together to handle HTTP requests.

Architecture Overview

Every REST request flows through four layers:

HTTP Request → Route → Controller → Service → Repository → Database
  • Routes map HTTP methods and paths to controller methods
  • Controllers parse requests, validate input, call services, and return responses
  • Services contain business logic and orchestrate repository calls
  • Repositories interact with the database via GORM

Controllers

Controllers live in app/rest/controllers/. Each controller receives a service through dependency injection and exposes handler methods for CRUD operations.

package controllers import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/gofastadev/gofasta/pkg/apperrors" "github.com/gofastadev/gofasta/pkg/httputil" "myapp/app/dtos" "myapp/app/services/interfaces" ) type ProductController struct { service interfaces.ProductService } func NewProductController(service interfaces.ProductService) *ProductController { return &ProductController{service: service} } func (c *ProductController) Create(w http.ResponseWriter, r *http.Request) error { var req dtos.CreateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return apperrors.BadRequest("invalid request body") } result, err := c.service.Create(r.Context(), req) if err != nil { return err } return httputil.Created(w, result) } func (c *ProductController) List(w http.ResponseWriter, r *http.Request) error { params := httputil.ParsePaginationParams(r) products, total, err := c.service.FindAll(r.Context(), params) if err != nil { return err } return httputil.PaginatedJSON(w, products, total, params) } func (c *ProductController) GetByID(w http.ResponseWriter, r *http.Request) error { id := chi.URLParam(r, "id") product, err := c.service.FindByID(r.Context(), id) if err != nil { return err } return httputil.OK(w, product) } func (c *ProductController) Update(w http.ResponseWriter, r *http.Request) error { id := chi.URLParam(r, "id") var req dtos.UpdateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return apperrors.BadRequest("invalid request body") } result, err := c.service.Update(r.Context(), id, req) if err != nil { return err } return httputil.OK(w, result) } func (c *ProductController) Archive(w http.ResponseWriter, r *http.Request) error { id := chi.URLParam(r, "id") if err := c.service.Delete(r.Context(), id); err != nil { return err } return httputil.NoContent(w) }

Routes

Routes live in app/rest/routes/. Each resource has its own route file that registers paths and maps them to controller methods.

package routes import ( "github.com/go-chi/chi/v5" "github.com/gofastadev/gofasta/pkg/httputil" "myapp/app/rest/controllers" ) func ProductRoutes(r chi.Router, ctrl *controllers.ProductController) { r.Get("/products", httputil.Handle(ctrl.List)) r.Post("/products", httputil.Handle(ctrl.Create)) r.Get("/products/{id}", httputil.Handle(ctrl.GetByID)) r.Put("/products/{id}", httputil.Handle(ctrl.Update)) r.Delete("/products/{id}", httputil.Handle(ctrl.Archive)) }

All resource route files are registered in app/rest/routes/index.routes.go, which is called from cmd/serve.go:

package routes import "github.com/go-chi/chi/v5" func RegisterRoutes(api chi.Router, deps *di.Container) { UserRoutes(api, deps.UserController) ProductRoutes(api, deps.ProductController) }

DTOs (Data Transfer Objects)

DTOs live in app/dtos/ and define the shape of request and response payloads. They keep your API contract separate from your database models.

package dtos import "time" type CreateProductRequest struct { Name string `json:"name" validate:"required"` Price float64 `json:"price" validate:"required,gt=0"` } type UpdateProductRequest struct { Name *string `json:"name"` Price *float64 `json:"price" validate:"omitempty,gt=0"` } type ProductResponse struct { ID string `json:"id"` Name string `json:"name"` Price float64 `json:"price"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` }

Use pointer fields in update DTOs to distinguish between “not provided” (nil) and “set to zero value”.

Response Helpers

The github.com/gofastadev/gofasta/pkg/httputil package provides consistent response formatting across your API.

Standard Responses

HelperStatus CodeUse Case
httputil.OK(w, data)200Successful read or update
httputil.Created(w, data)201Successful resource creation
httputil.NoContent(w)204Successful deletion
httputil.BadRequest(w, msg)400Validation or parsing failure
httputil.Unauthorized(w, msg)401Missing or invalid auth token
httputil.Forbidden(w, msg)403Insufficient permissions
httputil.NotFound(w, msg)404Resource not found
httputil.HandleError(w, err)variesMaps error types to status codes automatically

All responses follow a consistent JSON envelope:

{ "success": true, "data": { ... }, "message": "Resource created successfully" }

Error responses follow the same structure:

{ "success": false, "error": "Product not found", "message": "The requested resource does not exist" }

Pagination

httputil.ParsePaginationParams extracts page, per_page, sort, and order from query parameters with sensible defaults:

// GET /api/v1/products?page=2&per_page=20&sort=created_at&order=desc params := httputil.ParsePaginationParams(r) // params.Page = 2, params.PerPage = 20, params.Sort = "created_at", params.Order = "desc"

httputil.PaginatedJSON wraps the response with pagination metadata:

{ "success": true, "data": [ ... ], "meta": { "page": 2, "per_page": 20, "total": 143, "total_pages": 8 } }

Request Decoding

Use json.NewDecoder from the standard library to parse request bodies, and go-playground/validator/v10 struct tags for validation:

var req dtos.CreateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return apperrors.BadRequest("invalid request body") }

Middleware

Gofasta includes built-in middleware from github.com/gofastadev/gofasta/pkg/middleware. Middleware is applied in cmd/serve.go when the server starts.

import ( "github.com/go-chi/chi/v5" "github.com/gofastadev/gofasta/pkg/middleware" ) router := chi.NewRouter() router.Use(middleware.Logger()) router.Use(middleware.Recovery()) router.Use(middleware.CORS(middleware.CORSConfig{ AllowOrigins: []string{"http://localhost:3000"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Authorization", "Content-Type"}, })) router.Use(middleware.RateLimiter(middleware.RateLimiterConfig{ Max: 100, Window: time.Minute, }))

Available Middleware

MiddlewareDescription
middleware.Logger()Structured request logging with duration
middleware.Recovery()Panic recovery with stack trace logging
middleware.CORS(config)Cross-origin resource sharing
middleware.RateLimiter(config)Request rate limiting per IP
middleware.Auth(jwtConfig)JWT token verification
middleware.RBAC(enforcer)Casbin role-based access control
middleware.RequestID()Adds a unique request ID to each request

Protecting Routes

Apply auth middleware to specific route groups:

func RegisterRoutes(api chi.Router, deps *di.Container) { // Public routes AuthRoutes(api, deps.AuthController) // Protected routes — chi's Group runs the shared middleware for every // route registered inside the callback without mounting at a new prefix. api.Group(func(protected chi.Router) { protected.Use(middleware.Auth(deps.JWTConfig)) ProductRoutes(protected, deps.ProductController) OrderRoutes(protected, deps.OrderController) }) // Admin-only routes — Route mounts the callback at a sub-path. api.Route("/admin", func(admin chi.Router) { admin.Use(middleware.Auth(deps.JWTConfig)) admin.Use(middleware.RBAC(deps.Enforcer)) AdminRoutes(admin, deps.AdminController) }) }

Adding a New Endpoint

The fastest way to add a new resource is with the scaffold generator:

gofasta g scaffold Order total:float status:string user_id:uuid

This generates the model, migration, repository, service, controller, routes, DTOs, and DI provider, then wires everything together automatically.

To add a custom endpoint to an existing controller, add the handler method and register the route:

// In app/rest/controllers/product.controller.go func (c *ProductController) Search(w http.ResponseWriter, r *http.Request) error { query := r.URL.Query().Get("q") products, err := c.service.Search(r.Context(), query) if err != nil { return err } return httputil.OK(w, products) } // In app/rest/routes/product.routes.go r.Get("/search", httputil.Handle(ctrl.Search))

Swapping the Router

The scaffold uses go-chi/chi  as its default HTTP router because it builds on net/http, has zero external dependencies, and adds ergonomic subrouters and middleware groups. Like every other pkg/* default in Gofasta, the router is an opt-out default, not a required dependency — you can swap it for gorilla/mux, go-chi v4, or even Go 1.22+‘s standard http.ServeMux without touching the rest of the project.

What “swapping the router” means

Every REST-facing file in a generated project touches the router in exactly one of three ways: it constructs the root router, it registers routes on a passed-in router argument, or it reads a path parameter. Replacing chi means editing those three touch points consistently across the files listed below. No code in the gofasta library depends on chi — so once these files compile, you’re done.

Files to edit

FileWhat to change
app/rest/routes/index.routes.goReplace chi.NewRouter() with your router constructor. Replace r.Mount("/api/v1", api) with your router’s subrouter/prefix equivalent.
app/rest/routes/*.routes.go (one per resource)Change the r chi.Router parameter type and the r.Get / r.Post / r.Put / r.Delete calls to your router’s API.
app/rest/controllers/*.controller.go (one per resource)Replace every chi.URLParam(r, "id") call with your router’s path-parameter helper.
cmd/serve.goIf your replacement router does not return http.Handler, adjust the top-level http.NewServeMux() mount at mux.Handle("/", apiRouter).
go.modgo get the replacement and remove the chi require line (run go mod tidy to clean up).

That’s it — the gofasta library (github.com/gofastadev/gofasta/pkg/*) never imports any router. Middleware in pkg/middleware is plain func(http.Handler) http.Handler, so it works with any router that accepts net/http middleware.

Example — swapping to gorilla/mux

// app/rest/routes/user.routes.go package routes import ( "github.com/gorilla/mux" "github.com/gofastadev/gofasta/pkg/httputil" "myapp/app/rest/controllers" ) func UserRoutes(r *mux.Router, uc *controllers.UserController) { r.HandleFunc("/users", httputil.Handle(uc.ListUsers)).Methods("GET") r.HandleFunc("/users/{id}", httputil.Handle(uc.GetUser)).Methods("GET") // ... }
// app/rest/controllers/user.controller.go func (uc *UserController) GetUser(w http.ResponseWriter, r *http.Request) error { id := mux.Vars(r)["id"] // ... }

Example — swapping to stdlib http.ServeMux (Go 1.22+)

// app/rest/routes/user.routes.go package routes import ( "net/http" "github.com/gofastadev/gofasta/pkg/httputil" "myapp/app/rest/controllers" ) func UserRoutes(mux *http.ServeMux, uc *controllers.UserController) { mux.HandleFunc("GET /users", httputil.Handle(uc.ListUsers)) mux.HandleFunc("GET /users/{id}", httputil.Handle(uc.GetUser)) // ... }
// app/rest/controllers/user.controller.go func (uc *UserController) GetUser(w http.ResponseWriter, r *http.Request) error { id := r.PathValue("id") // ... }

Stdlib ServeMux has no built-in subrouter or per-group middleware. If you need those, either prefix every route manually or use http.StripPrefix with a nested *http.ServeMux.

gofasta routes after a swap

The gofasta routes introspection command parses route files for chi’s method-based API (r.Get, r.Post, etc.) and r.Mount(...) calls. If you swap routers, gofasta routes will return an empty list — it won’t crash your app, but the CLI-side listing stops working. You can either stop using it, or vendor a small fork of cli/internal/commands/routes.go that matches your router’s syntax.

Next Steps

Last updated on