Tutorial: Build a REST API
Learn to build a complete REST API with Goose from scratch.
What We'll Build
A simple blog API with:
- User authentication
- CRUD for blog posts
- Comments on posts
- Request validation
Prerequisites
- Go 1.21+
- Goose CLI installed
Step 1: Create the Project
goose app --name=blog-api --template=api
cd blog-api
go mod tidy
Step 2: Define Entities
Create app/entities/user.go:
package entities
import "time"
type User struct {
ID string `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex"`
Password string `json:"-"`
Name string `json:"name"`
CreatedAt *time.Time `json:"created_at"`
}
Create app/entities/post.go:
package entities
import "time"
type Post struct {
ID string `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Content string `json:"content"`
AuthorID string `json:"author_id"`
Author *User `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
Published bool `json:"published" gorm:"default:false"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
Step 3: Create Auth Module
Generate the module:
goose g module --name=auth --type=plain
Update app/auth/auth.service.go:
package auth
import (
"blog-api/app/entities"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type AuthService struct {
db *gorm.DB `inject:""`
jwtSecret string
}
func (s *AuthService) SetSecret(secret string) {
s.jwtSecret = secret
}
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func (s *AuthService) Register(email, password, name string) (*entities.User, error) {
// Check if user exists
var existing entities.User
if s.db.Where("email = ?", email).First(&existing).Error == nil {
return nil, errors.New("email already registered")
}
// Hash password
hashed, _ := bcrypt.GenerateFromPassword([]byte(password), 10)
user := &entities.User{
ID: generateID(),
Email: email,
Password: string(hashed),
Name: name,
}
if err := s.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (s *AuthService) Login(email, password string) (string, error) {
var user entities.User
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
return "", errors.New("invalid credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return "", errors.New("invalid credentials")
}
// Generate JWT
claims := Claims{
UserID: user.ID,
Email: user.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.jwtSecret))
}
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(s.jwtSecret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
Update app/auth/auth.controller.go:
package auth
import "github.com/awesome-goose/goose/types"
type AuthController struct {
service *AuthService `inject:""`
}
type RegisterDTO struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Name string `json:"name" validate:"required"`
}
type LoginDTO struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
func (c *AuthController) Register(ctx types.Context) any {
var dto RegisterDTO
if err := ctx.Bind(&dto); err != nil {
return ctx.Status(400).JSON(map[string]string{"error": "Invalid request"})
}
user, err := c.service.Register(dto.Email, dto.Password, dto.Name)
if err != nil {
return ctx.Status(400).JSON(map[string]string{"error": err.Error()})
}
return ctx.Status(201).JSON(user)
}
func (c *AuthController) Login(ctx types.Context) any {
var dto LoginDTO
if err := ctx.Bind(&dto); err != nil {
return ctx.Status(400).JSON(map[string]string{"error": "Invalid request"})
}
token, err := c.service.Login(dto.Email, dto.Password)
if err != nil {
return ctx.Status(401).JSON(map[string]string{"error": err.Error()})
}
return map[string]string{"token": token}
}
Update app/auth/auth.routes.go:
package auth
import "github.com/awesome-goose/goose/types"
func (c *AuthController) Routes() types.Routes {
return types.Routes{
{Method: "POST", Path: "/auth/register", Handler: c.Register},
{Method: "POST", Path: "/auth/login", Handler: c.Login},
}
}
Step 4: Create Posts Module
Generate the module:
goose g module --name=posts --type=resource
Update app/posts/posts.service.go:
package posts
import (
"blog-api/app/entities"
"gorm.io/gorm"
)
type PostsService struct {
db *gorm.DB `inject:""`
}
func (s *PostsService) GetAll() []entities.Post {
var posts []entities.Post
s.db.Where("published = ?", true).Preload("Author").Find(&posts)
return posts
}
func (s *PostsService) GetByID(id string) *entities.Post {
var post entities.Post
if err := s.db.Preload("Author").First(&post, "id = ?", id).Error; err != nil {
return nil
}
return &post
}
func (s *PostsService) GetByAuthor(authorID string) []entities.Post {
var posts []entities.Post
s.db.Where("author_id = ?", authorID).Find(&posts)
return posts
}
func (s *PostsService) Create(authorID string, dto CreatePostDTO) (*entities.Post, error) {
post := &entities.Post{
ID: generateID(),
Title: dto.Title,
Content: dto.Content,
AuthorID: authorID,
}
if err := s.db.Create(post).Error; err != nil {
return nil, err
}
return post, nil
}
func (s *PostsService) Update(id string, dto UpdatePostDTO) (*entities.Post, error) {
var post entities.Post
if err := s.db.First(&post, "id = ?", id).Error; err != nil {
return nil, err
}
post.Title = dto.Title
post.Content = dto.Content
post.Published = dto.Published
if err := s.db.Save(&post).Error; err != nil {
return nil, err
}
return &post, nil
}
func (s *PostsService) Delete(id string) error {
return s.db.Delete(&entities.Post{}, "id = ?", id).Error
}
Update app/posts/posts.controller.go:
package posts
import "github.com/awesome-goose/goose/types"
type PostsController struct {
service *PostsService `inject:""`
}
type CreatePostDTO struct {
Title string `json:"title" validate:"required"`
Content string `json:"content" validate:"required"`
}
type UpdatePostDTO struct {
Title string `json:"title"`
Content string `json:"content"`
Published bool `json:"published"`
}
func (c *PostsController) Index(ctx types.Context) any {
return c.service.GetAll()
}
func (c *PostsController) Show(ctx types.Context) any {
post := c.service.GetByID(ctx.Param("id"))
if post == nil {
return ctx.Status(404).JSON(map[string]string{"error": "Post not found"})
}
return post
}
func (c *PostsController) Create(ctx types.Context) any {
var dto CreatePostDTO
if err := ctx.Bind(&dto); err != nil {
return ctx.Status(400).JSON(map[string]string{"error": "Invalid request"})
}
authorID := ctx.Get("user_id").(string)
post, err := c.service.Create(authorID, dto)
if err != nil {
return ctx.Status(500).JSON(map[string]string{"error": err.Error()})
}
return ctx.Status(201).JSON(post)
}
func (c *PostsController) Update(ctx types.Context) any {
var dto UpdatePostDTO
if err := ctx.Bind(&dto); err != nil {
return ctx.Status(400).JSON(map[string]string{"error": "Invalid request"})
}
post, err := c.service.Update(ctx.Param("id"), dto)
if err != nil {
return ctx.Status(500).JSON(map[string]string{"error": err.Error()})
}
return post
}
func (c *PostsController) Delete(ctx types.Context) any {
if err := c.service.Delete(ctx.Param("id")); err != nil {
return ctx.Status(500).JSON(map[string]string{"error": err.Error()})
}
return ctx.Status(204).Send("")
}
func (c *PostsController) MyPosts(ctx types.Context) any {
authorID := ctx.Get("user_id").(string)
return c.service.GetByAuthor(authorID)
}
Step 5: Create Auth Middleware
Create app/auth/auth.middleware.go:
package auth
import (
"strings"
"github.com/awesome-goose/goose/types"
)
type AuthMiddleware struct {
service *AuthService `inject:""`
}
func (m *AuthMiddleware) Handle(ctx types.Context, next types.Next) any {
authHeader := ctx.Header("Authorization")
if authHeader == "" {
return ctx.Status(401).JSON(map[string]string{"error": "Missing authorization"})
}
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := m.service.ValidateToken(token)
if err != nil {
return ctx.Status(401).JSON(map[string]string{"error": "Invalid token"})
}
ctx.Set("user_id", claims.UserID)
ctx.Set("user_email", claims.Email)
return next()
}
Step 6: Update Routes
Update app/posts/posts.routes.go:
package posts
import (
"blog-api/app/auth"
"github.com/awesome-goose/goose/types"
)
func (c *PostsController) Routes() types.Routes {
authMiddleware := &auth.AuthMiddleware{}
return types.Routes{
// Public routes
{Method: "GET", Path: "/posts", Handler: c.Index},
{Method: "GET", Path: "/posts/:id", Handler: c.Show},
// Protected routes
{Method: "POST", Path: "/posts", Handler: c.Create,
Middlewares: []any{authMiddleware}},
{Method: "PUT", Path: "/posts/:id", Handler: c.Update,
Middlewares: []any{authMiddleware}},
{Method: "DELETE", Path: "/posts/:id", Handler: c.Delete,
Middlewares: []any{authMiddleware}},
{Method: "GET", Path: "/my-posts", Handler: c.MyPosts,
Middlewares: []any{authMiddleware}},
}
}
Step 7: Update App Module
Update app/app.module.go:
package app
import (
"blog-api/app/auth"
"blog-api/app/entities"
"blog-api/app/posts"
"github.com/awesome-goose/goose/types"
)
type AppModule struct{}
func (m *AppModule) Imports() []types.Module {
return []types.Module{
&auth.AuthModule{},
&posts.PostsModule{},
}
}
func (m *AppModule) Exports() []any {
return []any{}
}
func (m *AppModule) Declarations() []any {
return []any{
&MigrationService{},
}
}
Create app/migrations.service.go:
package app
import (
"blog-api/app/entities"
"gorm.io/gorm"
)
type MigrationService struct {
db *gorm.DB `inject:""`
}
func (s *MigrationService) OnStart() {
s.db.AutoMigrate(
&entities.User{},
&entities.Post{},
)
}
Step 8: Run the API
go run main.go
Step 9: Test the API
Register a user
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password123","name":"John"}'
Login
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password123"}'
Create a post (with token)
curl -X POST http://localhost:8080/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"title":"My First Post","content":"Hello World!"}'
Get all posts
curl http://localhost:8080/posts
Summary
You've built a REST API with:
- User authentication (JWT)
- CRUD operations for posts
- Route protection with middleware
- Database migrations
- Input validation
Next Steps
- Add comments feature
- Implement pagination
- Add search functionality
- Add API rate limiting