backend-service-patterns

Backend Service Patterns - Go/Echo/pgx for laneweaverTMS

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "backend-service-patterns" with this command: npx skills add linehaul-ai/linehaulai-claude-marketplace/linehaul-ai-linehaulai-claude-marketplace-backend-service-patterns

Backend Service Patterns - Go/Echo/pgx for laneweaverTMS

When to Use This Skill

  • Implementing new API endpoints (handlers)

  • Adding business logic (services)

  • Writing database queries (repositories)

  • Creating models and DTOs

  • Handling transactions

  • Building complex filters with QueryBuilder

  • Adding validation logic

  • Documenting APIs with OpenAPI 3.0 / Swagger annotations

Layered Architecture

HTTP Request → Handler → Service → Repository → PostgreSQL ↓ ↓ ↓ Bind/JSON Business SQL/pgx Validate Logic Transaction ↓ ↓ ↓ HTTP Response ← Handler ← Service ← Repository

Layer Responsibilities:

  • Handler (/internal/handlers/ ): HTTP request/response, Echo context, bind JSON, return APIResponse

  • Service (/internal/services/ ): Business logic, validation, orchestration, coordinate repos

  • Repository (/internal/repository/ ): SQL queries, transactions, pgx operations

  • Models (/internal/models/ ): Structs with db/json tags, enums, request/response DTOs

Handler Patterns

Handler Struct with Dependency Injection

type LoadHandler struct { service *services.LoadService }

func NewLoadHandler(service *services.LoadService) *LoadHandler { return &LoadHandler{service: service} }

Create Handler (POST)

func (h *LoadHandler) Create(c echo.Context) error { var req models.CreateLoadRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, models.APIResponse{ Success: false, Message: "Invalid request body", Errors: map[string][]string{"body": {"Failed to parse JSON"}}, }) }

result, err := h.service.CreateLoad(c.Request().Context(), &req)
if err != nil {
    // Validation errors → 400
    if validationErrors, ok := err.(models.ValidationErrors); ok {
        return c.JSON(http.StatusBadRequest, models.APIResponse{
            Success: false,
            Message: "Validation failed",
            Errors:  validationErrors.ToMap(),
        })
    }
    // Operational errors → 500
    c.Logger().Error("CreateLoad error:", err)
    return c.JSON(http.StatusInternalServerError, models.APIResponse{
        Success: false,
        Message: "Failed to create load",
    })
}

return c.JSON(http.StatusCreated, models.APIResponse{
    Success: true,
    Message: "Load created successfully",
    Data:    result,
})

}

List Handler with Filters (GET)

func (h *LoadHandler) List(c echo.Context) error { filters := parseLoadFilters(c)

items, total, err := h.service.ListLoads(c.Request().Context(), filters)
if err != nil {
    return c.JSON(http.StatusInternalServerError, models.APIResponse{
        Success: false,
        Message: "Failed to fetch loads",
    })
}

response := models.NewPaginatedResponse(items, total, filters.Page, filters.PageSize)
return c.JSON(http.StatusOK, models.APIResponse{
    Success: true,
    Data:    response,
})

}

Query Parameter Parsing (Nil Pointer Helpers)

func parseLoadFilters(c echo.Context) models.LoadFilters { var filters models.LoadFilters

// Multi-select (comma-separated)
if loadStatus := c.QueryParam("load_status"); loadStatus != "" {
    filters.LoadStatus = strings.Split(loadStatus, ",")
}

// Optional string
filters.AccountID = parseStringPtr(c.QueryParam("account_id"))

// Date range
filters.PickupDateFrom = parseDatePtr(c.QueryParam("pickup_date_from"))
filters.PickupDateTo = parseDatePtr(c.QueryParam("pickup_date_to"))

// Pagination with defaults
filters.Page = 1
if page := c.QueryParam("page"); page != "" {
    if p, err := strconv.Atoi(page); err == nil && p > 0 {
        filters.Page = p
    }
}
filters.PageSize = 50
if pageSize := c.QueryParam("page_size"); pageSize != "" {
    if ps, err := strconv.Atoi(pageSize); err == nil && ps > 0 && ps <= 100 {
        filters.PageSize = ps
    }
}

return filters

}

// Helper functions for safe type conversion func parseStringPtr(s string) *string { if s == "" { return nil } return &s }

func parseDatePtr(s string) *time.Time { if s == "" { return nil } t, err := time.Parse("2006-01-02", s) if err != nil { return nil } return &t }

func parseFloatPtr(s string) *float64 { if s == "" { return nil } f, err := strconv.ParseFloat(s, 64) if err != nil { return nil } return &f }

func parseBoolPtr(s string) *bool { if s == "" { return nil } b, err := strconv.ParseBool(s) if err != nil { return nil } return &b }

GetByID Handler

func (h *LoadHandler) GetByID(c echo.Context) error { id := c.Param("id")

load, err := h.service.GetLoadByID(c.Request().Context(), id)
if err != nil {
    return c.JSON(http.StatusInternalServerError, models.APIResponse{
        Success: false,
        Message: "Failed to fetch load",
    })
}
if load == nil {
    return c.JSON(http.StatusNotFound, models.APIResponse{
        Success: false,
        Message: "Load not found",
    })
}

return c.JSON(http.StatusOK, models.APIResponse{
    Success: true,
    Data:    load,
})

}

Service Patterns

Service Struct with Multiple Repositories

type LoadService struct { loadRepo *repository.LoadRepository facilityRepo *repository.FacilityRepository accountRepo *repository.AccountRepository }

func NewLoadService( loadRepo *repository.LoadRepository, facilityRepo *repository.FacilityRepository, accountRepo *repository.AccountRepository, ) *LoadService { return &LoadService{ loadRepo: loadRepo, facilityRepo: facilityRepo, accountRepo: accountRepo, } }

Create Method with Validation

func (s *LoadService) CreateLoad(ctx context.Context, req *models.CreateLoadRequest) (*models.CreateLoadResponse, error) { // 1. Validate request (delegated to models) if errors := models.ValidateCreateLoadRequest(req); len(errors) > 0 { return nil, errors }

// 2. Business logic (find-or-create facilities, generate IDs, etc.)
stopParams, err := s.resolveStopFacilities(ctx, req.Stops)
if err != nil {
    return nil, err
}

// 3. Build entity
load := &models.Load{
    ID:           uuid.New().String(),
    LoadNumber:   s.generateLoadNumber(ctx),
    TenderID:     req.TenderID,
    LoadStatus:   models.LoadStatusUncovered,
    Mode:         &req.Mode,
    CustomerRate: &req.CustomerRate,
}

// 4. Call repository
createParams := repository.CreateLoadParams{Load: load, Stops: stopParams}
if err := s.loadRepo.CreateWithStops(ctx, createParams); err != nil {
    return nil, fmt.Errorf("failed to create load: %w", err)
}

// 5. Build response
return &models.CreateLoadResponse{
    ID:         load.ID,
    LoadNumber: load.LoadNumber,
    LoadStatus: load.LoadStatus,
}, nil

}

Retry Logic for Race Conditions

const maxRetries = 3

func (s *LoadService) CreateLoadWithRetry(ctx context.Context, req *models.CreateLoadRequest) (*models.Load, error) { var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { loadNumber, err := s.generateLoadNumber(ctx) if err != nil { return nil, err }

    load := &#x26;models.Load{
        ID:         uuid.New().String(),
        LoadNumber: loadNumber,
        // ...
    }

    err = s.loadRepo.Create(ctx, load)
    if err != nil {
        // Retry on unique constraint violation
        if strings.Contains(err.Error(), "duplicate key") ||
            strings.Contains(err.Error(), "loads_load_number_key") {
            lastErr = err
            continue
        }
        return nil, err
    }
    return load, nil
}
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)

}

Repository Patterns

Repository Struct

type LoadRepository struct { db *database.Client }

func NewLoadRepository(db *database.Client) *LoadRepository { return &LoadRepository{db: db} }

Transaction Pattern

func (r *LoadRepository) CreateWithStops(ctx context.Context, params CreateLoadParams) error { tx, err := r.db.Begin(ctx) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx)

// Insert load
loadQuery := `
    INSERT INTO loads (id, load_number, tender_id, load_status, mode, customer_rate, created_at, updated_at)
    VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
`
_, err = tx.Exec(ctx, loadQuery,
    params.Load.ID,
    params.Load.LoadNumber,
    params.Load.TenderID,
    params.Load.LoadStatus,
    params.Load.Mode,
    params.Load.CustomerRate,
)
if err != nil {
    return fmt.Errorf("failed to insert load: %w", err)
}

// Insert stops
stopQuery := `
    INSERT INTO stops (id, load_id, facility_id, stop_sequence, stop_type, scheduled_date, created_at, updated_at)
    VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
`
for _, stop := range params.Stops {
    _, err = tx.Exec(ctx, stopQuery,
        stop.ID, params.Load.ID, stop.FacilityID, stop.StopSequence, stop.StopType, stop.ScheduledDate,
    )
    if err != nil {
        return fmt.Errorf("failed to insert stop %d: %w", stop.StopSequence, err)
    }
}

if err = tx.Commit(ctx); err != nil {
    return fmt.Errorf("failed to commit: %w", err)
}
return nil

}

GetByID with Not Found Handling

func (r *LoadRepository) GetByID(ctx context.Context, id string) (*models.Load, error) { query := SELECT id, load_number, tender_id, load_status, mode, customer_rate, created_at, updated_at FROM loads WHERE id = $1 AND deleted_at IS NULL var load models.Load err := r.db.QueryRow(ctx, query, id).Scan( &load.ID, &load.LoadNumber, &load.TenderID, &load.LoadStatus, &load.Mode, &load.CustomerRate, &load.CreatedAt, &load.UpdatedAt, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, nil // Not found } return nil, err } return &load, nil }

QueryBuilder for Complex Filters

func buildLoadListQuery(filters models.LoadFilters) (string, []interface{}) { qb := NewQueryBuilder()

// Multi-select IN conditions
qb.AddInCondition("l.load_status", filters.LoadStatus)
qb.AddInCondition("l.mode", filters.Mode)

// Single value conditions
if filters.AccountID != nil {
    qb.AddCondition("t.account_id = $%d", *filters.AccountID)
}
if filters.CarrierID != nil {
    qb.AddCondition("l.carrier_id = $%d", *filters.CarrierID)
}

// Date ranges
if filters.PickupDateFrom != nil {
    qb.AddCondition("t.pickup_date >= $%d", *filters.PickupDateFrom)
}
if filters.PickupDateTo != nil {
    qb.AddCondition("t.pickup_date &#x3C;= $%d", *filters.PickupDateTo)
}

// Boolean filters
if filters.IsCancelled != nil {
    qb.AddCondition("l.is_cancelled = $%d", *filters.IsCancelled)
}

// Text search
if filters.Search != nil &#x26;&#x26; *filters.Search != "" {
    qb.AddOrCondition([]string{"l.load_number", "a.name"}, "%"+*filters.Search+"%")
}

whereClause, args := qb.Build()

// Sort column whitelist
sortColumn := "l.created_at"
sortColumnMap := map[string]string{
    "load_number": "l.load_number",
    "created_at":  "l.created_at",
    "pickup_date": "t.pickup_date",
}
if col, ok := sortColumnMap[filters.SortBy]; ok {
    sortColumn = col
}

sortDir := "DESC"
if filters.SortDir == "asc" {
    sortDir = "ASC"
}

offset := (filters.Page - 1) * filters.PageSize
argIdx := qb.ArgCount()

baseQuery := `
    SELECT l.id, l.load_number, l.load_status, l.customer_rate, a.name as account_name
    FROM loads l
    JOIN tenders t ON l.tender_id = t.id
    LEFT JOIN accounts a ON t.account_id = a.id
    WHERE l.deleted_at IS NULL
` + whereClause

query := fmt.Sprintf("%s ORDER BY %s %s LIMIT $%d OFFSET $%d",
    baseQuery, sortColumn, sortDir, argIdx+1, argIdx+2)
args = append(args, filters.PageSize, offset)

return query, args

}

Find-or-Create Pattern

func (r *FacilityRepository) FindOrCreate(ctx context.Context, addr *models.FacilityAddress) (*models.Facility, error) { // Try to find existing existing, err := r.FindByAddress(ctx, addr.Address, addr.City, addr.State, addr.Zip) if err != nil { return nil, err } if existing != nil { return existing, nil }

// Create new
facility := &#x26;models.Facility{
    ID:      uuid.New().String(),
    Name:    fmt.Sprintf("%s, %s", addr.City, addr.State),
    Address: addr.Address,
    City:    addr.City,
    State:   strings.ToUpper(addr.State),
    Zip:     addr.Zip,
    Country: "USA",
}
if err := r.Create(ctx, facility); err != nil {
    return nil, err
}
return facility, nil

}

Model Patterns

Entity Struct with Tags

type Load struct { ID string db:"id" json:"id" LoadNumber string db:"load_number" json:"loadNumber" TenderID string db:"tender_id" json:"tenderId" CarrierID *string db:"carrier_id" json:"carrierId,omitempty" LoadStatus LoadStatus db:"load_status" json:"loadStatus" Mode *ModeOfTransport db:"mode" json:"mode,omitempty" CustomerRate *float64 db:"customer_rate" json:"customerRate,omitempty" CarrierRate *float64 db:"carrier_rate" json:"carrierRate,omitempty" IsCancelled bool db:"is_cancelled" json:"isCancelled"

// Audit fields
CreatedAt time.Time  `db:"created_at" json:"createdAt"`
UpdatedAt time.Time  `db:"updated_at" json:"updatedAt"`
CreatedBy *int32     `db:"created_by" json:"createdBy,omitempty"`
UpdatedBy *int32     `db:"updated_by" json:"updatedBy,omitempty"`
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`

}

Tag Conventions:

  • db:"column_name"

  • Maps to PostgreSQL column (snake_case)

  • json:"fieldName"

  • Maps to JSON field (camelCase for API)

  • json:"...,omitempty"

  • Omits null/zero values from response

  • Required fields: string , float64 , time.Time

  • Optional fields: *string , *float64 , *time.Time

Enum Definition with Validation

type LoadStatus string

const ( LoadStatusUncovered LoadStatus = "uncovered" LoadStatusAssigned LoadStatus = "assigned" LoadStatusDispatched LoadStatus = "dispatched" LoadStatusAtOrigin LoadStatus = "at_origin" LoadStatusInTransit LoadStatus = "in_transit" LoadStatusAtDestination LoadStatus = "at_destination" LoadStatusDelivered LoadStatus = "delivered" )

func (s LoadStatus) IsValid() bool { switch s { case LoadStatusUncovered, LoadStatusAssigned, LoadStatusDispatched, LoadStatusAtOrigin, LoadStatusInTransit, LoadStatusAtDestination, LoadStatusDelivered: return true } return false }

type ModeOfTransport string

const ( ModeDryVan ModeOfTransport = "Dry Van" ModeRefrigeratedVan ModeOfTransport = "Refrigerated Van" ModeFlatbed ModeOfTransport = "Flatbed" )

func (m ModeOfTransport) IsTempControlled() bool { return m == ModeRefrigeratedVan }

Request DTO

type CreateLoadRequest struct { TenderID string json:"tenderId" validate:"required" Mode ModeOfTransport json:"mode" validate:"required" CustomerRate float64 json:"customerRate" validate:"required,gt=0" CarrierRate *float64 json:"carrierRate,omitempty" validate:"omitempty,gt=0" Temperature *float64 json:"temperature,omitempty" Stops []CreateStopRequest json:"stops" validate:"required,min=2" }

Validation Function

func ValidateCreateLoadRequest(req *CreateLoadRequest) ValidationErrors { var errors ValidationErrors

if req.TenderID == "" {
    errors = append(errors, ValidationError{
        Field:   "tenderId",
        Message: "Tender ID is required",
    })
}

if !req.Mode.IsValid() {
    errors = append(errors, ValidationError{
        Field:   "mode",
        Message: "Invalid mode of transport",
    })
}

// Conditional validation
if req.Mode.IsTempControlled() &#x26;&#x26; req.Temperature == nil {
    errors = append(errors, ValidationError{
        Field:   "temperature",
        Message: fmt.Sprintf("Temperature is required for %s mode", req.Mode),
    })
}

if len(req.Stops) &#x3C; 2 {
    errors = append(errors, ValidationError{
        Field:   "stops",
        Message: "At least 2 stops are required (pickup and delivery)",
    })
}

return errors

}

Response & Error Patterns

APIResponse Wrapper

type APIResponse struct { Success bool json:"success" Message string json:"message,omitempty" Data interface{} json:"data,omitempty" Errors map[string][]string json:"errors,omitempty" }

PaginatedResponse Generic

type PaginatedResponse[T any] struct { Data []T json:"data" Total int json:"total" Page int json:"page" PageSize int json:"pageSize" TotalPages int json:"totalPages" }

func NewPaginatedResponse[T any](items []T, total, page, pageSize int) PaginatedResponse[T] { totalPages := 0 if total > 0 && pageSize > 0 { totalPages = (total + pageSize - 1) / pageSize } return PaginatedResponse[T]{ Data: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, } }

ValidationErrors Type

type ValidationError struct { Field string json:"field" Message string json:"message" }

type ValidationErrors []ValidationError

func (v ValidationErrors) Error() string { if len(v) == 0 { return "" } return fmt.Sprintf("%d validation error(s)", len(v)) }

func (v ValidationErrors) ToMap() map[string][]string { result := make(map[string][]string) for _, err := range v { result[err.Field] = append(result[err.Field], err.Message) } return result }

Router Setup

func Setup(e *echo.Echo, loadHandler *handlers.LoadHandler, accountHandler *handlers.AccountHandler) { // Global middleware e.Use(middleware.SetupLogger()) e.Use(middleware.SetupRecover()) e.Use(middleware.SetupCORS())

// Health check
e.GET("/health", func(c echo.Context) error {
    return c.JSON(200, map[string]string{"status": "ok"})
})

// API v1 routes
api := e.Group("/api/v1")

// Loads
loads := api.Group("/loads")
loads.GET("", loadHandler.List)
loads.POST("", loadHandler.Create)
loads.GET("/:id", loadHandler.GetByID)
loads.PUT("/:id", loadHandler.Update)
loads.DELETE("/:id", loadHandler.Delete)

// Accounts
accounts := api.Group("/accounts")
accounts.GET("", accountHandler.List)
accounts.GET("/:id", accountHandler.GetByID)

}

OpenAPI 3.0 Documentation (swaggo/echo-swagger)

Installation & Setup

Install swag CLI and echo-swagger:

go install github.com/swaggo/swag/cmd/swag@latest go get github.com/swaggo/echo-swagger

Generate docs (run from project root):

swag init -g cmd/server/main.go -o docs

Register swagger middleware in router:

import ( echoSwagger "github.com/swaggo/echo-swagger" _ "yourproject/docs" // Generated docs )

func Setup(e echo.Echo, ...) { // Swagger docs endpoint e.GET("/swagger/", echoSwagger.WrapHandler)

// ... rest of routes

}

Main Package Annotations

Add to cmd/server/main.go :

// @title laneweaverTMS API // @version 1.0 // @description Transportation Management System API

// @host localhost:8080 // @BasePath /api/v1

func main() { // ... }

Handler Annotations

Create Handler (POST)

// Create godoc // @Summary Create a new load // @Description Creates a load with stops and validates required fields // @Tags loads // @Accept json // @Produce json // @Param request body models.CreateLoadRequest true "Load creation request" // @Success 201 {object} models.APIResponse{data=models.CreateLoadResponse} // @Failure 400 {object} models.APIResponse // @Failure 500 {object} models.APIResponse // @Router /loads [post] func (h *LoadHandler) Create(c echo.Context) error { // ... existing implementation }

List Handler (GET with Query Params)

// List godoc // @Summary List loads with filters // @Description Returns paginated list of loads with optional filtering // @Tags loads // @Accept json // @Produce json // @Param load_status query string false "Filter by status (comma-separated)" Example(uncovered,assigned) // @Param account_id query string false "Filter by account ID" // @Param pickup_date_from query string false "Filter pickup date from (YYYY-MM-DD)" // @Param pickup_date_to query string false "Filter pickup date to (YYYY-MM-DD)" // @Param page query int false "Page number" default(1) // @Param page_size query int false "Items per page" default(50) maximum(100) // @Param sort_by query string false "Sort column" Enums(load_number,created_at,pickup_date) // @Param sort_dir query string false "Sort direction" Enums(asc,desc) // @Success 200 {object} models.APIResponse{data=models.PaginatedResponse[models.LoadListItem]} // @Failure 500 {object} models.APIResponse // @Router /loads [get] func (h *LoadHandler) List(c echo.Context) error { // ... existing implementation }

GetByID Handler (GET with Path Param)

// GetByID godoc // @Summary Get load by ID // @Description Returns a single load with full details // @Tags loads // @Accept json // @Produce json // @Param id path string true "Load ID" // @Success 200 {object} models.APIResponse{data=models.Load} // @Failure 404 {object} models.APIResponse // @Failure 500 {object} models.APIResponse // @Router /loads/{id} [get] func (h *LoadHandler) GetByID(c echo.Context) error { // ... existing implementation }

Model Annotations

Add example tags for documentation:

type CreateLoadRequest struct { TenderID string json:"tenderId" example:"tender-123" Mode ModeOfTransport json:"mode" example:"Dry Van" CustomerRate float64 json:"customerRate" example:"1500.00" CarrierRate *float64 json:"carrierRate,omitempty" example:"1200.00" Stops []CreateStopRequest json:"stops" }

type CreateLoadResponse struct { ID string json:"id" example:"load-456" LoadNumber string json:"loadNumber" example:"LD-2024-001" LoadStatus LoadStatus json:"loadStatus" example:"uncovered" }

For enums, add a comment listing valid values:

// LoadStatus represents the current state of a load // @Description Load lifecycle status // @Enum uncovered,assigned,dispatched,at_origin,in_transit,at_destination,delivered type LoadStatus string

Regenerating Docs

Run after any annotation changes:

swag init -g cmd/server/main.go -o docs

Access docs at: http://localhost:8080/swagger/index.html

Checklist for New Endpoints

Handler: [ ] Create handler struct with service dependency [ ] Implement New*Handler constructor [ ] Bind request body with c.Bind() [ ] Use type assertion for ValidationErrors [ ] Return APIResponse wrapper for all responses [ ] Log errors only on 500s

Service: [ ] Create service struct with repository dependencies [ ] Implement New*Service constructor [ ] Call validation function at start of create/update methods [ ] Use context.Context throughout [ ] Return ValidationErrors for business rule failures

Repository: [ ] Create repository struct with database client [ ] Use parameterized queries ($1, $2) [ ] Check pgx.ErrNoRows for "not found" [ ] Include deleted_at IS NULL in queries [ ] Use transactions for multi-table operations

Models: [ ] Add db and json tags to all fields [ ] Use pointers for nullable fields [ ] Include audit fields (CreatedAt, UpdatedAt, etc.) [ ] Create request/response DTOs separate from entity

Router: [ ] Register routes in Setup function [ ] Use route groups for resource prefixes [ ] Follow RESTful conventions (GET/POST/PUT/DELETE)

OpenAPI/Swagger: [ ] Add swagger annotations to handler functions [ ] Add example tags to request/response models [ ] Run swag init after annotation changes [ ] Verify docs at /swagger/index.html

Code References

  • Handlers: /internal/handlers/load.go , /internal/handlers/account.go

  • Services: /internal/services/load.go

  • Repositories: /internal/repository/load.go , /internal/repository/query_builder.go

  • Models: /internal/models/load.go , /internal/models/validation.go , /internal/models/responses.go

  • Router: /internal/router/router.go

Additional Documentation

  • Error Handling: For comprehensive error handling patterns beyond the examples shown here, see effective-go/references/error-handling.md (error wrapping, sentinel errors, custom types, panic/recover, testing)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

geospatial-postgis-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

rbac-authorization-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

quickbooks-online-api

No summary provided by upstream source.

Repository SourceNeeds Review
General

slack-block-kit

No summary provided by upstream source.

Repository SourceNeeds Review