Each part of your application should have a well-defined responsibility:
-
Handlers:
- Focus: Handling HTTP requests, routing, input validation (basic format checks), and output formatting (JSON responses).
- Minimal Logic: Keep business logic and data access logic out of handlers. They should act as thin orchestrators.
- Authentication & Authorization (Initial): Check if the user is authenticated (using middleware like your
AuthMiddleware
). You can also perform initial authorization checks based on readily available information (e.g., is the user trying to access their own profile?).
-
Services:
- Focus: Encapsulating core business logic and workflows. This includes complex validation rules, data transformations, authorization logic (based on roles, permissions, object ownership, etc.), and interactions with multiple repositories.
- Domain Experts: Services are the "experts" in your domain (books, users, etc.) and should contain the rules that govern how these entities interact.
-
Repositories:
- Focus: Data access. Repositories abstract away the database interactions. They provide methods for CRUD operations (Create, Read, Update, Delete) and other data retrieval logic.
- No Business Logic: Repositories should not contain business rules. They are purely responsible for communicating with the database.
-
Handlers:
- Initial authentication (is the user logged in?).
- Simple authorization based on readily available data (e.g., is the user trying to access their own profile?).
-
Services:
- Complex Authorization: Checking if the user has the required permissions to perform an action on a specific book. This often involves:
- Retrieving the book from the repository.
- Retrieving the bookshelf from the repository (to check ownership).
- Retrieving user data (roles, permissions) if necessary.
- Evaluating if the user's permissions and the book's context allow the action.
- Complex Authorization: Checking if the user has the required permissions to perform an action on a specific book. This often involves:
Example:
// In BookService.Update:
func (s *BookService) Update(ctx context.Context, bookID string, update *models.BookUpdate) error {
// 1. Get the book
book, err := s.repo.GetByID(ctx, bookID)
if err != nil {
return fmt.Errorf("failed to get book: %w", err)
}
// 2. Get the bookshelf
bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID)
if err != nil {
return fmt.Errorf("failed to get bookshelf: %w", err)
}
// 3. Get userID from context
userID, ok := ctx.Value("userID").(string)
if !ok {
return fmt.Errorf("userID not found in context")
}
// 4. Check bookshelf ownership
if bookshelf.UserID != userID {
return fmt.Errorf("user is not authorized to modify this book")
}
// 5. If authorized, proceed with the update:
if err := s.repo.Update(ctx, bookID, update); err != nil {
return fmt.Errorf("failed to update book: %w", err)
}
return nil
}
- Testability: Keeping handlers thin makes them easier to test. You can focus on testing request handling and response formatting.
- Maintainability: If business logic changes, you only need to update the service, not the handler.
- Code Clarity: It's easier to understand the flow of your application when concerns are separated.
- Start Simple: Begin with basic separation. As your application grows, you can refine the layers further.
- Don't Over-Engineer: Avoid adding unnecessary layers of abstraction if they're not needed.
- Testing is Crucial: Write tests for all your layers (handlers, services, repositories).