8.8 KiB
API Development Guide
Step-by-step guide to building new API endpoints following Clean Architecture patterns.
Overview
Building an API endpoint involves these layers:
- Domain - Entity and repository interface
- Infrastructure - Repository implementation
- Application - Service with business logic
- API - HTTP handler and routes
Step 1: Database Layer
Create Migration
Add migration files in src/pkg/db/postgres/sqlc/migrations/:
-- 000015_create_resources.up.sql
CREATE TABLE app.resources (
id SERIAL PRIMARY KEY,
organization_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_resources_org ON app.resources(organization_id);
Write SQL Queries
In src/pkg/db/postgres/sqlc/query/resources.sql:
-- name: GetResourceByID :one
SELECT * FROM app.resources WHERE id = $1;
-- name: CreateResource :one
INSERT INTO app.resources (organization_id, name, status)
VALUES ($1, $2, $3)
RETURNING *;
-- name: ListResources :many
SELECT * FROM app.resources
WHERE organization_id = $1
ORDER BY created_at DESC;
Generate Code
make sqlc
Create Store Interface
In src/pkg/db/adapters/resource_store.go:
type ResourceStore interface {
GetResourceByID(ctx context.Context, id int32) (sqlc.Resource, error)
CreateResource(ctx context.Context, arg sqlc.CreateResourceParams) (sqlc.Resource, error)
ListResources(ctx context.Context, orgID int32) ([]sqlc.Resource, error)
}
Implement Adapter
In src/pkg/db/postgres/adapter_impl/resource_store.go:
type resourceStore struct {
store sqlc.Store
}
func NewResourceStore(store sqlc.Store) adapters.ResourceStore {
return &resourceStore{store: store}
}
func (s *resourceStore) GetResourceByID(ctx context.Context, id int32) (sqlc.Resource, error) {
return s.store.GetResourceByID(ctx, id)
}
Register in DI
In src/pkg/db/inject.go:
container.Provide(func(sqlcStore sqlc.Store) adapters.ResourceStore {
return adapter_impl.NewResourceStore(sqlcStore)
})
Step 2: Domain Layer
Create Entity
In src/app/resources/domain/entity.go:
type Resource struct {
ID int32
OrganizationID int32
Name string
Status string
CreatedAt time.Time
UpdatedAt time.Time
}
func (r *Resource) Validate() error {
if r.Name == "" {
return ErrResourceNameRequired
}
return nil
}
Define Repository Interface
In src/app/resources/domain/repository.go:
type ResourceRepository interface {
Create(ctx context.Context, resource *Resource) (*Resource, error)
GetByID(ctx context.Context, id int32) (*Resource, error)
List(ctx context.Context, orgID int32) ([]*Resource, error)
}
Step 3: Infrastructure Layer
Implement Repository
In src/app/resources/infra/repositories/resource_repository.go:
type resourceRepository struct {
store adapters.ResourceStore
}
func NewResourceRepository(store adapters.ResourceStore) domain.ResourceRepository {
return &resourceRepository{store: store}
}
func (r *resourceRepository) Create(ctx context.Context, resource *domain.Resource) (*domain.Resource, error) {
params := sqlc.CreateResourceParams{
OrganizationID: resource.OrganizationID,
Name: resource.Name,
Status: resource.Status,
}
dbResource, err := r.store.CreateResource(ctx, params)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
return toDomainResource(dbResource), nil
}
Step 4: Application Layer
Define Service Interface
In src/app/resources/app/services/resource_service_interface.go:
type ResourceService interface {
CreateResource(ctx context.Context, orgID int32, req *CreateResourceRequest) (*domain.Resource, error)
GetResource(ctx context.Context, id int32) (*domain.Resource, error)
ListResources(ctx context.Context, orgID int32) ([]*domain.Resource, error)
}
Implement Service
In src/app/resources/app/services/resource_service.go:
type resourceService struct {
repo domain.ResourceRepository
}
func NewResourceService(repo domain.ResourceRepository) ResourceService {
return &resourceService{repo: repo}
}
func (s *resourceService) CreateResource(
ctx context.Context,
orgID int32,
req *CreateResourceRequest,
) (*domain.Resource, error) {
// Validate request
if err := req.Validate(); err != nil {
return nil, err
}
// Create entity
resource := &domain.Resource{
OrganizationID: orgID,
Name: req.Name,
Status: "active",
}
// Persist
return s.repo.Create(ctx, resource)
}
Step 5: API Layer
Create Handler
In src/api/resources/handler.go:
type Handler struct {
service services.ResourceService
}
func NewHandler(service services.ResourceService) *Handler {
return &Handler{service: service}
}
func (h *Handler) CreateResource(c *gin.Context) {
// Get auth context
reqCtx := auth.GetRequestContext(c)
if reqCtx == nil {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
// Parse request
var req services.CreateResourceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid request"})
return
}
// Call service
resource, err := h.service.CreateResource(c.Request.Context(), reqCtx.OrganizationID, &req)
if err != nil {
c.JSON(500, gin.H{"error": "failed to create resource"})
return
}
c.JSON(201, resource)
}
Register Routes
In src/api/resources/routes.go:
type Routes struct {
handler *Handler
authMiddleware *auth.Middleware
}
func NewRoutes(handler *Handler, authMiddleware *auth.Middleware) *Routes {
return &Routes{handler: handler, authMiddleware: authMiddleware}
}
func (r *Routes) Register(router *gin.Engine) {
apiGroup := router.Group("/api/resources")
apiGroup.Use(r.authMiddleware.RequireAuth())
apiGroup.Use(r.authMiddleware.RequireOrganization())
{
apiGroup.POST("",
auth.RequirePermissionFunc("resource", "create"),
r.handler.CreateResource)
apiGroup.GET("/:id", r.handler.GetResource)
apiGroup.GET("", r.handler.ListResources)
}
}
Step 6: Module Registration
Create Module
In src/app/resources/module.go:
type Module struct {
container *dig.Container
}
func NewModule(container *dig.Container) *Module {
return &Module{container: container}
}
func (m *Module) RegisterDependencies() error {
// Repository
if err := m.container.Provide(func(store adapters.ResourceStore) domain.ResourceRepository {
return repositories.NewResourceRepository(store)
}); err != nil {
return err
}
// Service
if err := m.container.Provide(func(repo domain.ResourceRepository) services.ResourceService {
return services.NewResourceService(repo)
}); err != nil {
return err
}
return nil
}
Initialize Module
In src/app/resources/cmd/init.go:
func Init(container *dig.Container) error {
module := NewModule(container)
return module.RegisterDependencies()
}
Register API
In src/api/resources/provider.go:
func RegisterDependencies(container *dig.Container) error {
// Register handler
if err := container.Provide(func(service services.ResourceService) *Handler {
return NewHandler(service)
}); err != nil {
return err
}
// Register routes
if err := container.Provide(func(
handler *Handler,
authMiddleware *auth.Middleware,
) *Routes {
return NewRoutes(handler, authMiddleware)
}); err != nil {
return err
}
return nil
}
Quick Reference
File Structure
src/app/resources/
├── domain/
│ ├── entity.go
│ ├── repository.go
│ └── errors.go
├── app/services/
│ ├── resource_service_interface.go
│ └── resource_service.go
├── infra/repositories/
│ └── resource_repository.go
├── cmd/init.go
└── module.go
src/api/resources/
├── handler.go
├── routes.go
└── provider.go
Common Response Codes
200- Success201- Created400- Bad Request401- Unauthorized403- Forbidden404- Not Found500- Internal Server Error
Next Steps
- Add tests: Unit tests for service, integration tests for repository
- Add Swagger docs: Document API with Swagger annotations
- Add validation: Request/response validation
- Add events: Publish domain events for cross-module communication