My backend template using Golang + Fiber
- Golang 1.24.0 or newer
- golangci-lint
- pre-commit
-
Rename Go module name:
go mod edit -module YOUR_MODULE_NAME
Example:
go mod edit -module github.com/yourusername/yourprojectname
-
Find all occurrences of
github.com/yokeTH/gofiber-template/internal
and replace them withYOUR_MODULE_NAME
:find . -type f -name '*.go' -exec sed -i '' 's|github.com/yokeTH/gofiber-template|YOUR_MODULE_NAME|g' {} +
Install pre-commit and set up hooks:
brew install pre-commit
pre-commit install
Install and initialize commitlint to enforce commit message conventions:
go install github.com/conventionalcommit/commitlint@latest
commitlint init
Example commit message:
feat: add user authentication
After renaming the module, ensure dependencies are updated:
go mod tidy
The apperror
package ensures consistent and structured error handling across repositories, services, and handlers.
In the repository layer, if a database operation fails, an InternalServerError
is returned:
func (r *BookRepository) CreateBook(book *domain.Book) error {
if err := r.db.Create(book).Error; err != nil {
return apperror.InternalServerError(err, "failed to create book")
}
return nil
}
The service layer simply propagates the error from the repository:
func (s *BookService) CreateBook(book *domain.Book) error {
return s.BookRepository.CreateBook(book)
}
In the handler, errors are processed and returned appropriately:
func (h *BookHandler) CreateBook(c *fiber.Ctx) error {
body := new(dto.CreateBookRequest)
if err := c.BodyParser(body); err != nil {
return apperror.BadRequestError(err, err.Error())
}
book := &domain.Book{
Author: body.Author,
Title: body.Title,
}
if err := h.BookService.CreateBook(book); err != nil {
if apperror.IsAppError(err) {
return err // Return the structured AppError as-is
}
return apperror.InternalServerError(err, "create book service failed")
}
res := dto.BookResponse{
ID: book.ID,
Author: book.Author,
Title: book.Title,
}
return c.Status(fiber.StatusCreated).JSON(dto.Success(res))
}
benefit to using with swagger it wrapped with SuccessResponse
or ErrorResponse
(if return as apperror)
// for data with pagination
dto.SuccessPagination(data []T, currentPage int, lastPage int, limit int, total int)
// for data without pagination
dto.Success(data T)
swagger comment docs example:
// @response 201 {object} dto.SuccessResponse[dto.BookResponse] "Created"
// @response 400 {object} dto.ErrorResponse "Bad Request"
// @response 500 {object} dto.ErrorResponse "Internal Server Error"
Here is an example of how to use db.Pagination
in your repository:
func (r *BookRepository) GetBooks(limit, page, total, last *int) ([]domain.Book, error) {
var books []domain.Book
if err := r.db.Scopes(db.Paginate(domain.Book{}, limit, page, total, last)).Find(&books).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, apperror.NotFoundError(err, "books not found")
}
return nil, apperror.InternalServerError(err, "failed to get books")
}
return books, nil
}