
Software Backend
- 250 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
Helps with backend & apis tasks.
About
software-backend is a Claude Code skill for backend & apis. It helps solo builders move faster with AI-assisted development.
- software-backend
- Backend & APIs
- AI-coding skill
Software Backend by the numbers
- 250 all-time installs (skills.sh)
- +16 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,533 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill software-backendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 250 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
What it does
Helps with backend & apis tasks.
Files
Software Backend Engineering
Use this skill to design, implement, and review production-grade backend services: API boundaries, data layer, auth, caching, observability, error handling, testing, and deployment.
Defaults to bias toward: type-safe boundaries (validation at the edge), OpenTelemetry for observability, zero-trust assumptions, idempotency for retries, RFC 9457 errors, Postgres + pooling, structured logs, timeouts, and rate limiting.
Scaffolding rule: When scaffolding a new project, show full working implementations for all domain logic — fraud rules, audit logging, webhook handlers, validation pipelines, background jobs. Don't just reference file names or stub functions; show the actual code so the user can run it immediately.
---
Quick Reference
| Task | Default Picks | Notes |
|---|---|---|
| REST API | Fastify / Express / NestJS | Prefer typed boundaries + explicit timeouts |
| Edge API | Hono / platform-native handlers | Keep work stateless, CPU-light |
| Type-Safe API | tRPC | Prefer for TS monorepos and internal APIs |
| GraphQL API | Apollo Server / Pothos | Prefer for complex client-driven queries |
| Database | PostgreSQL | Use pooling + migrations + query budgets |
| ORM / Query Layer | Prisma / Drizzle / SQLAlchemy / GORM / SeaORM / EF Core | Prefer explicit transactions |
| Authentication | OIDC/OAuth + sessions/JWT | Prefer httpOnly cookies for browsers |
| Validation | Zod / Pydantic / validator libs | Validate at the boundary, not deep inside |
| Caching | Redis (or managed) | Use TTLs + invalidation strategy |
| Background Jobs | BullMQ / platform queues | Make jobs idempotent + retry-safe |
| Testing | Unit + integration + contract/E2E | Keep most tests below the UI layer |
| Observability | Structured logs + OpenTelemetry | Correlation IDs end-to-end |
Scope
Use this skill to:
- Design and implement REST/GraphQL/tRPC APIs
- Model data schemas and run safe migrations
- Implement authentication/authorization (OIDC/OAuth, sessions/JWT)
- Add validation, error handling, rate limiting, caching, and background jobs
- Ship production readiness (timeouts, observability, deploy/runbooks)
When NOT to Use This Skill
Use a different skill when:
- Frontend-only concerns -> See software-frontend
- Infrastructure provisioning (Terraform, K8s manifests) -> See ops-devops-platform
- API design patterns only (no implementation) -> See dev-api-design
- SQL query optimization and indexing -> See data-sql-optimization
- Security audits and threat modeling -> See software-security-appsec
- System architecture (beyond single service) -> See software-architecture-design
Technology Selection
Pick based on the strongest constraint, not feature lists:
| Constraint | Default Pick | Why |
|---|---|---|
| Team knows TypeScript only | Fastify/Hono + Prisma/Drizzle | Ecosystem depth, hiring ease |
| Need <50ms P95, CPU-bound work | Go (net/http + sqlc/pgx) | Goroutines isolate CPU work; no event-loop risk |
| Data-heavy / ML integration | Python (FastAPI + SQLAlchemy) | Best ecosystem for numpy/pandas/ML pipelines |
| Memory-safety critical | Rust (Axum + SeaORM/SQLx) | Zero-cost abstractions, no GC |
| Enterprise/.NET team | C# (ASP.NET Core + EF Core) | Azure integration, mature tooling |
| Edge/serverless | Hono / platform-native handlers | Stateless, CPU-light, fast cold starts |
| Fintech/audit-sensitive | Go + sqlc (or raw SQL) | ORM magic is a liability; you need auditable SQL |
For detailed framework/ORM/auth/caching selection trees, see references/edge-deployment-guide.md and language-specific references. See assets/ for starter templates per language.
---
API Design Patterns (Dec 2025)
Idempotency Patterns
All mutating operations MUST support idempotency for retry safety.
Implementation:
// Idempotency key header
const idempotencyKey = request.headers['idempotency-key'];
const cached = await redis.get(`idem:${idempotencyKey}`);
if (cached) return JSON.parse(cached);
const result = await processOperation();
await redis.set(`idem:${idempotencyKey}`, JSON.stringify(result), 'EX', 86400);
return result;| Do | Avoid |
|---|---|
| Store idempotency keys with TTL (24h typical) | Processing duplicate requests |
| Return cached response for duplicate keys | Different responses for same key |
| Use client-generated UUIDs | Server-generated keys |
Pagination Patterns
| Pattern | Use When | Example |
|---|---|---|
| Cursor-based | Large datasets, real-time data | ?cursor=abc123&limit=20 |
| Offset-based | Small datasets, random access | ?page=3&per_page=20 |
| Keyset | Sorted data, high performance | ?after_id=1000&limit=20 |
Prefer cursor-based pagination for APIs with frequent inserts.
Error Response Standard (Problem Details)
Use a consistent machine-readable error format (RFC 9457 Problem Details): https://www.rfc-editor.org/rfc/rfc9457
{
"type": "https://example.com/problems/invalid-request",
"title": "Invalid request",
"status": 400,
"detail": "email is required",
"instance": "/v1/users"
}Health Check Patterns
// Liveness: Is the process running?
app.get('/health/live', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// Readiness: Can the service handle traffic?
app.get('/health/ready', async (req, res) => {
const dbOk = await checkDatabase();
const cacheOk = await checkRedis();
if (dbOk && cacheOk) {
res.status(200).json({ status: 'ready', db: 'ok', cache: 'ok' });
} else {
res.status(503).json({ status: 'not ready', db: dbOk, cache: cacheOk });
}
});Common Mistakes (Non-Obvious)
| Avoid | Instead | Why |
|---|---|---|
| N+1 queries | include/select or DataLoader | 10-100x perf hit; easy to miss in ORM code |
| No request timeouts | Timeouts on HTTP clients, DB, handlers | Hung deps cascade; see Production Hardening below |
| Missing connection pooling | Prisma pool / PgBouncer / pgx pool | Exhaustion under load on shared DB tiers |
| Catching errors silently | Log + rethrow or handle explicitly | Hidden failures, impossible to debug |
---
Production Hardening: Patterns Models Skip
These are the patterns that separate "works in dev" from "survives production." Models tend to skip them unless explicitly prompted — add them to every service.
Request & Query Timeouts
Every outbound call needs a timeout. Without one, a hung dependency leaks connections and cascades failures.
// HTTP client timeout
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
// Database query timeout (Prisma)
await prisma.$queryRaw`SET statement_timeout = '3000'`;
// Express/Fastify request timeout
server.register(import('@fastify/timeout'), { timeout: 30000 });| Layer | Default Timeout | Rationale |
|---|---|---|
| HTTP client calls | 5s | External APIs shouldn't block you |
| Database queries | 3s | Slow queries = missing index or bad plan |
| Request handler | 30s | Safety net for the whole request lifecycle |
| Background jobs | 5min | Jobs that run longer need chunking |
Field-Level Selection (Don't SELECT *)
ORMs default to fetching all columns. On wide tables this wastes bandwidth and hides performance problems.
// BAD: fetches all 30 columns
const users = await prisma.user.findMany({ include: { posts: true } });
// GOOD: fetch only what the endpoint needs
const users = await prisma.user.findMany({
select: { id: true, name: true, email: true },
include: { posts: { select: { id: true, title: true } } }
});For Go (sqlc): write explicit column lists in SQL queries — sqlc enforces this naturally. For Python (SQLAlchemy): use load_only() or explicit column selection.
Structured Error Responses (RFC 9457)
Return machine-readable errors from day one. Clients shouldn't have to regex-parse error messages.
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "email must be a valid email address",
"instance": "/v1/users",
"errors": [{ "field": "email", "message": "invalid format" }]
}Set Content-Type: application/problem+json. This format is a standard (RFC 9457) and parseable by any HTTP client.
Query Plan Verification
Before shipping any new query to production, verify its execution plan:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT ... FROM ... WHERE ...;Red flags in the output: Seq Scan on large tables, Nested Loop with high row estimates, Sort without index. Add indexes or rewrite the query before deploying.
---
Performance Debugging Workflow
When a service is slow, work through these layers in order. Fix the cheapest layer first — don't add caching before fixing N+1 queries.
| Step | What to Check | Fix |
|---|---|---|
| 1. Query analysis | Enable query logging, find N+1s and slow queries | Rewrite with include/joins, add select for field-level optimization |
| 2. Indexing | Run EXPLAIN ANALYZE on slow queries | Add composite indexes matching WHERE + ORDER BY patterns |
| 3. Connection pooling | Check connection count vs. pool size | Configure pool limits (Prisma connection_limit, PgBouncer, pgx pool) |
| 4. Caching | Identify read-heavy, rarely-changing data | Add Redis/in-memory cache with TTL + invalidation strategy |
| 5. Timeouts | Check for missing timeouts on DB, HTTP, handlers | Add timeouts at every layer (see Production Hardening above) |
| 6. Platform tuning | Shared DB limits, cold starts, memory | Upgrade tier, add read replicas, tune runtime settings |
Key principle: always measure before and after. Use structured logging with request IDs to trace specific slow requests end-to-end.
---
Infrastructure Economics
Backend architecture decisions directly impact cost and revenue. See references/infrastructure-economics.md for detailed cost modeling, SLA-to-revenue mapping, unit economics checklists, and FinOps practices.
---
Navigation
Resources
- references/backend-best-practices.md - Template authoring guide, quality checklist, and shared utilities pointers
- references/edge-deployment-guide.md - Edge computing patterns, Cloudflare Workers vs Vercel Edge, tRPC, Hono, Bun
- references/infrastructure-economics.md - Cost modeling, performance SLAs -> revenue, FinOps practices, cloud optimization
- references/go-best-practices.md - Go idioms, concurrency, error handling, GORM usage, testing, profiling
- references/rust-best-practices.md - Ownership, async, Axum, SeaORM, error handling, testing
- references/python-best-practices.md - FastAPI, SQLAlchemy, async patterns, validation, testing, performance
- references/nodejs-best-practices.md - Event loop, async patterns, Express/Fastify/NestJS/Hono, error handling, memory management, security, profiling
- references/csharp-best-practices.md - C# 14 / .NET 10 LTS, extension members, field keyword, ASP.NET Core 10 (validation, SSE, OpenAPI 3.1), EF Core 10 (LeftJoin, named filters), HybridCache, Polly v8 resilience
- references/database-patterns.md - PostgreSQL patterns (JSONB, CTEs, partitioning), connection pooling, migration strategies, ORM comparison, index design
- references/message-queues-background-jobs.md - BullMQ patterns, broker comparison (Redis/SQS/Kafka/RabbitMQ), idempotent jobs, DLQ, scheduling, delivery guarantees
- data/sources.json - External references per language/runtime
- Shared checklists: ../software-clean-code-standard/assets/checklists/backend-api-review-checklist.md, ../software-clean-code-standard/assets/checklists/secure-code-review-checklist.md
Shared Utilities (Centralized patterns - extract, don't duplicate)
- ../software-clean-code-standard/utilities/auth-utilities.md - Argon2id, jose JWT, OAuth 2.1/PKCE
- ../software-clean-code-standard/utilities/error-handling.md - Effect Result types, correlation IDs
- ../software-clean-code-standard/utilities/config-validation.md - Zod 3.24+, Valibot, secrets management
- ../software-clean-code-standard/utilities/resilience-utilities.md - p-retry v6, opossum v8, OTel spans
- ../software-clean-code-standard/utilities/logging-utilities.md - pino v9 + OpenTelemetry integration
- ../software-clean-code-standard/utilities/testing-utilities.md - Vitest, MSW v2, factories, fixtures
- ../software-clean-code-standard/utilities/observability-utilities.md - OpenTelemetry SDK, tracing, metrics
- ../software-clean-code-standard/references/clean-code-standard.md - Canonical clean code rules (
CC-*) for citation
Templates
- assets/nodejs/template-nodejs-prisma-postgres.md - Node.js + Prisma + PostgreSQL
- assets/go/template-go-fiber-gorm.md - Go + Fiber + GORM + PostgreSQL
- assets/rust/template-rust-axum-seaorm.md - Rust + Axum + SeaORM + PostgreSQL
- assets/python/template-python-fastapi-sqlalchemy.md - Python + FastAPI + SQLAlchemy + PostgreSQL
- assets/csharp/template-csharp-aspnet-efcore.md - C# + ASP.NET Core + Entity Framework Core + PostgreSQL
Related Skills
- ../software-architecture-design/SKILL.md - System decomposition, SLAs, and data flows
- ../software-security-appsec/SKILL.md - Authentication/authorization and secure API design
- ../ops-devops-platform/SKILL.md - CI/CD, infrastructure, and deployment safety
- ../qa-resilience/SKILL.md - Resilience, retries, and failure playbooks
- ../software-code-review/SKILL.md - Review checklists and standards for backend changes
- ../qa-testing-strategy/SKILL.md - Testing strategies, test pyramids, and coverage goals
- ../dev-api-design/SKILL.md - RESTful design, GraphQL, and API versioning patterns
- ../data-sql-optimization/SKILL.md - SQL optimization, indexing, and query tuning patterns
---
Freshness Protocol
When users ask version-sensitive recommendation questions, do a quick freshness check before asserting "best" choices or quoting versions.
Trigger Conditions
- "What's the best backend framework for [use case]?"
- "What should I use for [API design/auth/database]?"
- "What's the latest in Node.js/Go/Rust?"
- "Current best practices for [REST/GraphQL/tRPC]?"
- "Is [framework/runtime] still relevant in 2026?"
- "[Express] vs [Fastify] vs [Hono]?"
- "Best ORM for [database/use case]?"
How to Freshness-Check
1. Start from data/sources.json (official docs, release notes, support policies). 2. Run a targeted web search for the specific component and open release notes/support policy pages. 3. Prefer official sources over blogs for versions and support windows.
What to Report
- Current landscape: what is stable and widely used now
- Emerging trends: what is gaining traction (and why)
- Deprecated/declining: what is falling out of favor (and why)
- Recommendation: default choice + 1-2 alternatives, with trade-offs
Example Topics (verify with fresh search)
- Node.js LTS support window and major changes
- Bun vs Deno vs Node.js
- Hono, Elysia, and edge-first frameworks
- Drizzle vs Prisma for TypeScript
- tRPC and end-to-end type safety
- Edge computing and serverless patterns
- .NET 10 LTS (Nov 2025) and C# 14 adoption
- ASP.NET Core 10 built-in validation vs FluentValidation
- EF Core 10 vs Dapper for C# data access
- HybridCache vs manual IMemoryCache + IDistributedCache
---
Operational Playbooks
- references/operational-playbook.md - Full backend architecture patterns, checklists, TypeScript notes, and decision tables
Fact-Checking
- Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.
- Prefer primary sources; report source links and dates for volatile information.
- If web access is unavailable, state the limitation and mark guidance as unverified.
Backend Engineering - C# + ASP.NET Core + Entity Framework Core Template
Purpose: Enterprise-grade .NET 10 (LTS) APIs with C# 14, strong typing, DI, and mature tooling, ideal for large teams and Azure ecosystems
---
When to Use
Use this template when building:
- Enterprise APIs with complex business logic and domain modeling
- Services targeting Azure or Windows Server ecosystems
- Systems requiring strong type safety and compile-time guarantees
- High-performance services needing AOT compilation or gRPC
- Teams with .NET/C# expertise or enterprise backgrounds
- Microservices with mature DevOps pipelines (CI/CD, health checks, observability)
C#/ASP.NET Core Advantages:
- Built-in DI: First-class dependency injection container
- Performance: Kestrel is among the fastest web servers; AOT compilation available
- Strong typing: Compile-time safety with nullable reference types
- Mature ecosystem: Entity Framework Core, Identity, SignalR, gRPC, OpenAPI
- Cross-platform: Runs on Linux, macOS, Windows
- Enterprise tooling: Visual Studio, Rider, dotnet CLI, Azure DevOps
---
TEMPLATE STARTS HERE
1. Project Overview
Tech Stack:
- [ ] .NET 10 LTS (C# 14; released Nov 2025, supported through Nov 2028)
- [ ] ASP.NET Core 10 (Minimal API or Controller-based, built-in validation, OpenAPI 3.1)
- [ ] Entity Framework Core 10 (LeftJoin, named query filters, simplified ExecuteUpdate)
- [ ] PostgreSQL 16+ (via Npgsql provider)
- [ ] HybridCache + Redis (L1/L2 caching with stampede protection)
- [ ] Microsoft.Extensions.Resilience + Polly v8 (retry, circuit breaker, timeout)
- [ ] Serilog (structured logging)
- [ ] xUnit + Moq (testing)
- [ ] MediatR (optional, CQRS/mediator pattern)
Project Name: {{project_name}}
Team:
- Backend: {{team_size}} .NET developers
- DevOps: {{devops_team_size}} engineers
---
2. Project Structure
project-root/
|-- src/
| |-- {{ProjectName}}.Api/
| | |-- Controllers/
| | | |-- AuthController.cs
| | | `-- UsersController.cs
| | |-- Middleware/
| | | |-- ExceptionHandlingMiddleware.cs
| | | `-- CorrelationIdMiddleware.cs
| | |-- Filters/
| | | `-- ValidationFilter.cs
| | |-- Program.cs
| | |-- appsettings.json
| | |-- appsettings.Development.json
| | `-- {{ProjectName}}.Api.csproj
| |-- {{ProjectName}}.Application/
| | |-- DTOs/
| | | `-- UserDto.cs
| | |-- Interfaces/
| | | |-- IUserService.cs
| | | `-- IUserRepository.cs
| | |-- Services/
| | | `-- UserService.cs
| | |-- Validators/
| | | `-- CreateUserValidator.cs
| | `-- {{ProjectName}}.Application.csproj
| |-- {{ProjectName}}.Domain/
| | |-- Entities/
| | | `-- User.cs
| | |-- Exceptions/
| | | |-- DomainException.cs
| | | `-- NotFoundException.cs
| | |-- ValueObjects/
| | | `-- Email.cs
| | `-- {{ProjectName}}.Domain.csproj
| `-- {{ProjectName}}.Infrastructure/
| |-- Data/
| | |-- AppDbContext.cs
| | |-- Configurations/
| | | `-- UserConfiguration.cs
| | `-- Migrations/
| |-- Repositories/
| | `-- UserRepository.cs
| |-- Services/
| | `-- CacheService.cs
| `-- {{ProjectName}}.Infrastructure.csproj
|-- tests/
| |-- {{ProjectName}}.UnitTests/
| | |-- Services/
| | | `-- UserServiceTests.cs
| | `-- {{ProjectName}}.UnitTests.csproj
| `-- {{ProjectName}}.IntegrationTests/
| |-- ApiTests/
| | `-- UsersApiTests.cs
| |-- Fixtures/
| | `-- WebApplicationFixture.cs
| `-- {{ProjectName}}.IntegrationTests.csproj
|-- docker-compose.yml
|-- Dockerfile
|-- .editorconfig
|-- Directory.Build.props
|-- {{ProjectName}}.sln
`-- README.mdKey Principles:
- Clean Architecture (Domain -> Application -> Infrastructure -> API)
- Dependency Inversion: inner layers define interfaces, outer layers implement
- Entity Framework Core with code-first migrations
- Serilog structured logging with correlation IDs
- xUnit integration tests with WebApplicationFactory
---
Centralization Guide
Important: Shared patterns go in the Application/Infrastructure layers. Do not duplicate across projects.
| Utility | Extract To | Reference |
|---|---|---|
| Config (Options pattern) | Api/ service registration | config-validation.md |
| JWT (token generation/validation) | Infrastructure/Services/ | auth-utilities.md |
| Password hashing (BCrypt) | Infrastructure/Services/ | auth-utilities.md |
| Errors (ProblemDetails, middleware) | Api/Middleware/ | error-handling.md |
| Logging (Serilog setup) | Api/Program.cs | logging-utilities.md |
---
3. Environment Configuration
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=myapp;Username=postgres;Password=postgres",
"Redis": "localhost:6379"
},
"Jwt": {
"SecretKey": "your-super-secret-jwt-key-minimum-32-characters-long",
"Issuer": "MyApi",
"Audience": "MyApi",
"ExpirationMinutes": 60
},
"Cors": {
"AllowedOrigins": ["http://localhost:3000", "https://myapp.com"]
},
"RateLimiting": {
"PermitLimit": 100,
"WindowSeconds": 60
}
}Directory.Build.props
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
</Project>.editorconfig (excerpt)
[*.cs]
indent_style = space
indent_size = 4
# Prefer expression bodies
csharp_style_expression_bodied_methods = when_on_single_line
csharp_style_expression_bodied_properties = true
# Prefer primary constructors
csharp_style_prefer_primary_constructors = true
# Nullable
dotnet_diagnostic.CS8600.severity = error
dotnet_diagnostic.CS8602.severity = error
dotnet_diagnostic.CS8603.severity = error---
4. Domain Layer
Domain/Entities/User.cs
namespace MyApp.Domain.Entities;
public class User
{
public int Id { get; private set; }
public string Email { get; private set; } = null!;
public string HashedPassword { get; private set; } = null!;
public string FullName { get; private set; } = null!;
public bool IsActive { get; private set; } = true;
public DateTime CreatedAt { get; private set; }
public DateTime UpdatedAt { get; private set; }
public DateTime? DeletedAt { get; private set; }
private User() { } // EF Core needs parameterless constructor
public static User Create(string email, string hashedPassword, string fullName)
{
return new User
{
Email = email,
HashedPassword = hashedPassword,
FullName = fullName,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
}
public void UpdateProfile(string fullName)
{
FullName = fullName;
UpdatedAt = DateTime.UtcNow;
}
public void Deactivate()
{
IsActive = false;
DeletedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
}Domain/Exceptions/DomainException.cs
namespace MyApp.Domain.Exceptions;
public abstract class DomainException(string message, string code)
: Exception(message)
{
public string Code { get; } = code;
}
public class NotFoundException(string entity, object id)
: DomainException($"{entity} with id '{id}' was not found", "NOT_FOUND");
public class ConflictException(string message)
: DomainException(message, "CONFLICT");---
5. Infrastructure Layer
Infrastructure/Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Domain.Entities;
namespace MyApp.Infrastructure.Data;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}Infrastructure/Data/Configurations/UserConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MyApp.Domain.Entities;
namespace MyApp.Infrastructure.Data.Configurations;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("users");
builder.HasKey(u => u.Id);
builder.Property(u => u.Email).HasMaxLength(255).IsRequired();
builder.Property(u => u.HashedPassword).HasMaxLength(255).IsRequired();
builder.Property(u => u.FullName).HasMaxLength(255).IsRequired();
builder.Property(u => u.CreatedAt).HasDefaultValueSql("NOW()");
builder.Property(u => u.UpdatedAt).HasDefaultValueSql("NOW()");
builder.HasIndex(u => u.Email).IsUnique();
// Global query filter for soft delete
builder.HasQueryFilter(u => u.DeletedAt == null);
}
}Infrastructure/Repositories/UserRepository.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Application.Interfaces;
using MyApp.Domain.Entities;
using MyApp.Infrastructure.Data;
namespace MyApp.Infrastructure.Repositories;
public class UserRepository(AppDbContext context) : IUserRepository
{
public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
=> await context.Users.FirstOrDefaultAsync(u => u.Id == id, ct);
public async Task<User?> GetByEmailAsync(string email, CancellationToken ct = default)
=> await context.Users.FirstOrDefaultAsync(u => u.Email == email, ct);
public async Task AddAsync(User user, CancellationToken ct = default)
{
await context.Users.AddAsync(user, ct);
await context.SaveChangesAsync(ct);
}
public async Task UpdateAsync(User user, CancellationToken ct = default)
{
context.Users.Update(user);
await context.SaveChangesAsync(ct);
}
public async Task<IReadOnlyList<User>> ListAsync(
int skip = 0, int take = 20, CancellationToken ct = default)
{
return await context.Users
.OrderBy(u => u.Id)
.Skip(skip)
.Take(take)
.AsNoTracking()
.ToListAsync(ct);
}
}---
6. Application Layer
Application/Interfaces/IUserRepository.cs
using MyApp.Domain.Entities;
namespace MyApp.Application.Interfaces;
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id, CancellationToken ct = default);
Task<User?> GetByEmailAsync(string email, CancellationToken ct = default);
Task AddAsync(User user, CancellationToken ct = default);
Task UpdateAsync(User user, CancellationToken ct = default);
Task<IReadOnlyList<User>> ListAsync(int skip = 0, int take = 20, CancellationToken ct = default);
}Application/DTOs/UserDto.cs
namespace MyApp.Application.DTOs;
public record UserDto(int Id, string Email, string FullName, bool IsActive, DateTime CreatedAt);
public record CreateUserRequest(string Email, string Password, string FullName);
public record UpdateUserRequest(string? FullName);
public record LoginRequest(string Email, string Password);
public record TokenResponse(string AccessToken, string TokenType = "Bearer");Application/Services/UserService.cs
using MyApp.Application.DTOs;
using MyApp.Application.Interfaces;
using MyApp.Domain.Entities;
using MyApp.Domain.Exceptions;
namespace MyApp.Application.Services;
public class UserService(
IUserRepository userRepository,
IPasswordHasher passwordHasher,
ILogger<UserService> logger) : IUserService
{
public async Task<UserDto> CreateAsync(CreateUserRequest request, CancellationToken ct = default)
{
var existing = await userRepository.GetByEmailAsync(request.Email, ct);
if (existing is not null)
throw new ConflictException("Email already registered");
var hashedPassword = passwordHasher.Hash(request.Password);
var user = User.Create(request.Email, hashedPassword, request.FullName);
await userRepository.AddAsync(user, ct);
logger.LogInformation("User {UserId} created with email {Email}", user.Id, user.Email);
return ToDto(user);
}
public async Task<UserDto?> GetByIdAsync(int id, CancellationToken ct = default)
{
var user = await userRepository.GetByIdAsync(id, ct);
return user is null ? null : ToDto(user);
}
private static UserDto ToDto(User user) =>
new(user.Id, user.Email, user.FullName, user.IsActive, user.CreatedAt);
}---
7. API Layer
Api/Program.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Api.Middleware;
using MyApp.Application.Interfaces;
using MyApp.Application.Services;
using MyApp.Infrastructure.Data;
using MyApp.Infrastructure.Repositories;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Serilog
builder.Host.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter());
});
// Database (EF Core 10)
builder.Services.AddDbContextPool<AppDbContext>(options =>
{
options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConnection"),
npgsql => npgsql.EnableRetryOnFailure(3));
});
// DI
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
// HybridCache (.NET 10 — L1 in-memory + L2 Redis)
builder.Services.AddHybridCache();
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
// Resilience (Polly v8 via Microsoft.Extensions.Http.Resilience)
builder.Services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri(builder.Configuration["ExternalApi:BaseUrl"]!);
})
.AddStandardResilienceHandler();
// Validation (.NET 10 — source-generator based, automatic)
builder.Services.AddValidation();
// Auth
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
// API
builder.Services.AddControllers();
// OpenAPI 3.1 (.NET 10 — replaces Swashbuckle)
builder.Services.AddOpenApi();
// Health checks
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")!);
// CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
policy.WithOrigins(origins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
// Middleware pipeline
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseSerilogRequestLogging();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // Serves OpenAPI 3.1 doc
}
app.UseHttpsRedirection();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();
// Make Program accessible for integration tests
public partial class Program { }Api/Controllers/UsersController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MyApp.Application.DTOs;
using MyApp.Application.Interfaces;
namespace MyApp.Api.Controllers;
[ApiController]
[Route("api/v1/[controller]")]
[Produces("application/json")]
public class UsersController(IUserService userService) : ControllerBase
{
[HttpGet("{id}")]
[ProducesResponseType(typeof(UserDto), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetById(int id, CancellationToken ct)
{
var user = await userService.GetByIdAsync(id, ct);
return user is null ? NotFound() : Ok(user);
}
[HttpPost]
[ProducesResponseType(typeof(UserDto), 201)]
[ProducesResponseType(typeof(ValidationProblemDetails), 400)]
public async Task<IActionResult> Create(
[FromBody] CreateUserRequest request,
CancellationToken ct)
{
var user = await userService.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
}
}---
8. Testing
tests/UnitTests/Services/UserServiceTests.cs
using Moq;
using MyApp.Application.DTOs;
using MyApp.Application.Interfaces;
using MyApp.Application.Services;
using MyApp.Domain.Entities;
using MyApp.Domain.Exceptions;
namespace MyApp.UnitTests.Services;
public class UserServiceTests
{
private readonly Mock<IUserRepository> _repoMock = new();
private readonly Mock<IPasswordHasher> _hasherMock = new();
private readonly Mock<ILogger<UserService>> _loggerMock = new();
private readonly UserService _sut;
public UserServiceTests()
{
_sut = new UserService(_repoMock.Object, _hasherMock.Object, _loggerMock.Object);
}
[Fact]
public async Task CreateAsync_NewUser_ReturnsDto()
{
var request = new CreateUserRequest("test@example.com", "P@ssw0rd!", "Test User");
_repoMock.Setup(r => r.GetByEmailAsync(request.Email, default)).ReturnsAsync((User?)null);
_hasherMock.Setup(h => h.Hash(request.Password)).Returns("hashed");
var result = await _sut.CreateAsync(request);
Assert.Equal("test@example.com", result.Email);
_repoMock.Verify(r => r.AddAsync(It.IsAny<User>(), default), Times.Once);
}
[Fact]
public async Task CreateAsync_DuplicateEmail_ThrowsConflict()
{
var existing = User.Create("test@example.com", "hash", "Existing");
_repoMock.Setup(r => r.GetByEmailAsync("test@example.com", default)).ReturnsAsync(existing);
await Assert.ThrowsAsync<ConflictException>(() =>
_sut.CreateAsync(new("test@example.com", "pass", "New User")));
}
}tests/IntegrationTests/ApiTests/UsersApiTests.cs
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Application.DTOs;
using MyApp.Infrastructure.Data;
namespace MyApp.IntegrationTests.ApiTests;
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb_" + Guid.NewGuid()));
});
}).CreateClient();
}
[Fact]
public async Task CreateUser_ValidRequest_Returns201()
{
var request = new CreateUserRequest("test@example.com", "P@ssw0rd!", "Test User");
var response = await _client.PostAsJsonAsync("/api/v1/users", request);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var user = await response.Content.ReadFromJsonAsync<UserDto>();
Assert.Equal("test@example.com", user!.Email);
}
[Fact]
public async Task GetUser_NotFound_Returns404()
{
var response = await _client.GetAsync("/api/v1/users/999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}---
9. Docker Setup
Dockerfile
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.sln ./
COPY src/**/*.csproj ./src/
RUN for file in src/*/*.csproj; do \
dir=$(dirname "$file"); \
mkdir -p "$dir"; \
mv "$file" "$dir/"; \
done
RUN dotnet restore
COPY . .
RUN dotnet publish src/MyApp.Api/MyApp.Api.csproj -c Release -o /app/publish --no-restore
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
RUN adduser --disabled-password --gecos "" appuser
USER appuser
COPY --from=build /app/publish .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]docker-compose.yml
services:
api:
build: .
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=myapp;Username=postgres;Password=postgres
- ConnectionStrings__Redis=redis:6379
- Jwt__SecretKey=your-super-secret-jwt-key-minimum-32-chars
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:Makefile
.PHONY: build run test migrate lint
build:
dotnet build
run:
dotnet run --project src/MyApp.Api
test:
dotnet test --verbosity normal
test-coverage:
dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage
migrate-add:
dotnet ef migrations add $(name) --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
migrate-up:
dotnet ef database update --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
migrate-script:
dotnet ef migrations script --idempotent --project src/MyApp.Infrastructure --startup-project src/MyApp.Api -o migrations.sql
lint:
dotnet format --verify-no-changes
docker-up:
docker compose up -d
docker-down:
docker compose down
docker-logs:
docker compose logs -f api---
10. Production Checklist
Security
- [ ] JWT secret is strong (min 32 chars) and stored in secret manager
- [ ] Passkey / WebAuthn support via ASP.NET Core Identity (.NET 10)
- [ ] HTTPS enforced via Kestrel or reverse proxy
- [ ] Rate limiting per endpoint (built-in .NET 8+ rate limiter)
- [ ] CORS restricted to specific origins
- [ ] SQL injection prevention (EF Core parameterized queries; SQL injection analyzer in EF 10)
- [ ] Password hashing with BCrypt (work factor 12+) or ASP.NET Core Identity PasswordHasher
- [ ] Input validation with built-in Minimal API validation (.NET 10) or DataAnnotations
- [ ] Security headers (HSTS, X-Content-Type-Options, etc.)
- [ ] Dependency vulnerability scanning (
dotnet list package --vulnerable) - [ ] Configuration validated at startup (Options pattern + ValidateOnStart)
Performance
- [ ] DbContext pooling (
AddDbContextPool) - [ ] HybridCache for read-heavy operations (L1 in-memory + L2 Redis, stampede protection)
- [ ] Async/await for all I/O operations with CancellationToken propagation
- [ ] Database indexes on foreign keys and query columns
- [ ] Response compression (Brotli + GZip)
- [ ] Pagination for list endpoints (prefer cursor-based)
- [ ] Load testing with k6 or NBomber
- [ ] AsNoTracking for read-only queries; compiled queries for hot paths
- [ ] EF Core 10 LeftJoin instead of GroupJoin+SelectMany+DefaultIfEmpty
Observability
- [ ] Structured logging with Serilog (JSON format)
- [ ] Correlation ID tracking (X-Correlation-Id header)
- [ ] Error tracking (Sentry or Application Insights)
- [ ] APM integration (Application Insights, Datadog, or New Relic)
- [ ] Health check endpoints (/health/live, /health/ready)
- [ ] Metrics exposed (Prometheus via prometheus-net or OpenTelemetry)
- [ ] OpenTelemetry tracing
Deployment
- [ ] Multi-stage Docker build with non-root user
- [ ] Container security scanning
- [ ] EF Core migrations automated (or idempotent SQL scripts)
- [ ] Graceful shutdown configured (HostOptions.ShutdownTimeout)
- [ ] Zero-downtime deployment
- [ ] CI/CD pipeline (GitHub Actions, Azure DevOps)
- [ ] Blue-green or canary deployment
Reliability
- [ ] Database backups automated
- [ ] Redis persistence configured
- [ ] Retry logic with Microsoft.Extensions.Resilience + Polly v8 (
AddStandardResilienceHandler) - [ ] Timeouts on all HTTP clients and DB queries
- [ ] Circuit breaker for external services (Polly v8 pipelines)
- [ ] Background jobs with retry (Hangfire or custom IHostedService)
- [ ] Dead letter queue for failed messages
- [ ] Named query filters for soft-delete and multi-tenancy (EF Core 10)
---
END
Congratulations! You now have a production-grade C# 14 / .NET 10 backend with ASP.NET Core 10, Entity Framework Core 10, and PostgreSQL.
Next Steps: 1. Create solution: dotnet new sln -n MyApp 2. Create projects and add to solution 3. Copy appsettings.json and configure connection strings 4. Start services with docker compose up -d 5. Run migrations with make migrate-up 6. Start development server with make run 7. Access OpenAPI docs at https://localhost:5001/openapi/v1.json
C# 14 / .NET 10 Best Practices:
- Extension members (C# 14): Use
extensionblocks for extension properties and methods - `field` keyword (C# 14): Semi-auto properties — custom accessor logic without backing field boilerplate
- Null-conditional assignment (C# 14):
obj?.Prop = value;— cleaner null-safe code - Nullable reference types: Enable
<Nullable>enable</Nullable>project-wide - Primary constructors: Use for concise DI injection
- Records for DTOs: Immutable with value equality
- Options pattern: Strongly-typed configuration with validation
- CancellationToken: Propagate through all async methods
- Clean Architecture: Domain has no dependencies on infrastructure
- HybridCache: Replace manual IMemoryCache + IDistributedCache with HybridCache (L1+L2, stampede-safe)
- Microsoft.Extensions.Resilience: Use
AddStandardResilienceHandler()for HTTP clients (replaces raw Polly) - Built-in validation: Use
AddValidation()— source-generator based, AOT-compatible - OpenAPI 3.1: Use
AddOpenApi()+MapOpenApi()(replaces Swashbuckle) - EF Core 10 LeftJoin: Replace GroupJoin+SelectMany+DefaultIfEmpty with
.LeftJoin() - Named query filters: Tag filters by name, disable selectively with
.IgnoreQueryFilters(["name"]) - WebApplicationFactory: Integration tests with real HTTP pipeline
- Serilog: Structured logging with message templates (no string interpolation)
Backend Engineering - Go + Fiber + GORM Template
Purpose: High-performance, concurrent backend services with Go's native concurrency primitives
---
When to Use
Use this template when building:
- High-performance APIs requiring low latency (<10ms)
- Microservices with intensive concurrent operations
- Real-time systems (WebSocket servers, streaming APIs)
- Services processing millions of requests/day
- Systems requiring predictable memory usage and minimal GC pauses
Go Advantages:
- Native goroutines for lightweight concurrency (100k+ concurrent connections)
- Static compilation (single binary deployment, no runtime dependencies)
- Fast startup times (critical for serverless/containers)
- Built-in race detector and profiling tools
- Excellent standard library (HTTP, JSON, crypto)
---
TEMPLATE STARTS HERE
1. Project Overview
Tech Stack:
- [ ] Go 1.22+ (prefer latest stable)
- [ ] Fiber v2 (Express-inspired framework, 10x faster than Express.js)
- [ ] GORM 1.25+ (ORM with hooks, transactions, migrations)
- [ ] PostgreSQL 14+ (or org standard)
- [ ] Redis (caching, session store)
- [ ] github.com/golang-jwt/jwt/v5 (authentication)
- [ ] Testify (testing assertions)
- [ ] Air (hot reload for development)
Project Name: {{project_name}}
Team:
- Backend: {{team_size}} developers
- DevOps: {{devops_team}}
---
2. Project Structure
project-root/
|-- cmd/
| `-- api/
| `-- main.go # Application entry point
|-- internal/
| |-- api/
| | |-- handlers/ # HTTP handlers
| | |-- middleware/ # Custom middleware
| | |-- routes/ # Route definitions
| | `-- validators/ # Request validation
| |-- domain/
| | |-- models/ # Domain models (GORM structs)
| | |-- repositories/ # Data access interfaces
| | `-- services/ # Business logic
| |-- infrastructure/
| | |-- database/ # Database connection, migrations
| | |-- cache/ # Redis client
| | `-- config/ # Configuration loader
| `-- pkg/
| |-- auth/ # JWT utilities
| |-- errors/ # Custom error types
| |-- logger/ # Structured logging
| `-- utils/ # Shared utilities
|-- migrations/ # SQL migration files
|-- tests/
| |-- integration/
| `-- unit/
|-- scripts/
| |-- migrate.sh
| `-- seed.sh
|-- .air.toml # Hot reload config
|-- .env.example
|-- Dockerfile
|-- docker-compose.yml
|-- go.mod
|-- go.sum
|-- Makefile
`-- README.mdKey Principles:
cmd/for executable entry pointsinternal/for private application code (not importable by external packages)pkg/for reusable utilities- Clear separation: handlers -> services -> repositories -> database
---
Centralization Guide
Important: The code patterns in this template should be extracted to shared utility packages. Do not duplicate these utilities across services.
| Utility | Extract To | Reference |
|---|---|---|
Config loading (getEnv, getEnvInt) | internal/pkg/config/ | config-validation.md |
JWT (JWTManager, Generate, Verify) | internal/pkg/auth/ | auth-utilities.md |
Password (HashPassword, VerifyPassword) | internal/pkg/auth/ | auth-utilities.md |
Errors (AppError, errorHandler) | internal/pkg/errors/ | error-handling.md |
| Logging (zap setup) | internal/pkg/logger/ | logging-utilities.md |
Pattern: Create utilities once in internal/pkg/, import everywhere via:
import "myapp/internal/pkg/auth"
import "myapp/internal/pkg/errors"---
3. Environment Configuration
.env.example
# Server
APP_ENV=development
APP_PORT=8080
APP_NAME=your-api
APP_VERSION=1.0.0
# Database
DATABASE_URL=postgres://user:password@localhost:5432/dbname?sslmode=disable
DB_MAX_OPEN_CONNS=25
DB_MAX_IDLE_CONNS=5
DB_CONN_MAX_LIFETIME=5m
# Redis
REDIS_URL=redis://localhost:6379/0
REDIS_PASSWORD=
REDIS_DB=0
# JWT
JWT_SECRET=your-super-secret-key-min-32-chars
JWT_EXPIRATION=168h # 7 days
JWT_REFRESH_EXPIRATION=720h # 30 days
# CORS
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://yourapp.com
CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE
CORS_ALLOWED_HEADERS=Content-Type,Authorization
# Rate Limiting
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=1m
# Logging
LOG_LEVEL=info
LOG_FORMAT=jsoninternal/infrastructure/config/config.go
package config
import (
"fmt"
"os"
"strconv"
"time"
"github.com/joho/godotenv"
)
type Config struct {
App AppConfig
Database DatabaseConfig
Redis RedisConfig
JWT JWTConfig
CORS CORSConfig
RateLimit RateLimitConfig
Logger LoggerConfig
}
type AppConfig struct {
Env string
Port string
Name string
Version string
}
type DatabaseConfig struct {
URL string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
}
type RedisConfig struct {
URL string
Password string
DB int
}
type JWTConfig struct {
Secret string
Expiration time.Duration
RefreshExpiration time.Duration
}
type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
}
type RateLimitConfig struct {
Requests int
Window time.Duration
}
type LoggerConfig struct {
Level string
Format string
}
func Load() (*Config, error) {
// Load .env file in development
if os.Getenv("APP_ENV") != "production" {
if err := godotenv.Load(); err != nil {
return nil, fmt.Errorf("error loading .env file: %w", err)
}
}
cfg := &Config{
App: AppConfig{
Env: getEnv("APP_ENV", "development"),
Port: getEnv("APP_PORT", "8080"),
Name: getEnv("APP_NAME", "api"),
Version: getEnv("APP_VERSION", "1.0.0"),
},
Database: DatabaseConfig{
URL: getEnv("DATABASE_URL", ""),
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 5),
ConnMaxLifetime: getEnvDuration("DB_CONN_MAX_LIFETIME", 5*time.Minute),
},
Redis: RedisConfig{
URL: getEnv("REDIS_URL", "redis://localhost:6379/0"),
Password: getEnv("REDIS_PASSWORD", ""),
DB: getEnvInt("REDIS_DB", 0),
},
JWT: JWTConfig{
Secret: getEnv("JWT_SECRET", ""),
Expiration: getEnvDuration("JWT_EXPIRATION", 168*time.Hour),
RefreshExpiration: getEnvDuration("JWT_REFRESH_EXPIRATION", 720*time.Hour),
},
RateLimit: RateLimitConfig{
Requests: getEnvInt("RATE_LIMIT_REQUESTS", 100),
Window: getEnvDuration("RATE_LIMIT_WINDOW", 1*time.Minute),
},
Logger: LoggerConfig{
Level: getEnv("LOG_LEVEL", "info"),
Format: getEnv("LOG_FORMAT", "json"),
},
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
return cfg, nil
}
func (c *Config) Validate() error {
if c.Database.URL == "" {
return fmt.Errorf("DATABASE_URL is required")
}
if c.JWT.Secret == "" || len(c.JWT.Secret) < 32 {
return fmt.Errorf("JWT_SECRET must be at least 32 characters")
}
return nil
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intVal, err := strconv.Atoi(value); err == nil {
return intVal
}
}
return defaultValue
}
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}---
4. Database Setup
internal/infrastructure/database/postgres.go
package database
import (
"context"
"fmt"
"log"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"{{module_path}}/internal/domain/models"
"{{module_path}}/internal/infrastructure/config"
)
type Database struct {
*gorm.DB
}
func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) {
logLevel := logger.Info
if cfg.LogLevel == "silent" {
logLevel = logger.Silent
}
db, err := gorm.Open(postgres.Open(cfg.URL), &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
NowFunc: func() time.Time {
return time.Now().UTC()
},
PrepareStmt: true, // Prepared statement cache
})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get database instance: %w", err)
}
// Connection pool settings
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
// Verify connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := sqlDB.PingContext(ctx); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
log.Println("Database connection established")
return &Database{db}, nil
}
func (db *Database) AutoMigrate() error {
return db.DB.AutoMigrate(
&models.User{},
&models.Post{},
&models.Comment{},
// Add more models here
)
}
func (db *Database) Close() error {
sqlDB, err := db.DB.DB()
if err != nil {
return err
}
return sqlDB.Close()
}internal/domain/models/user.go
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
Password string `gorm:"not null" json:"-"` // Never serialize password
Name string `gorm:"not null" json:"name"`
Role string `gorm:"type:varchar(20);default:'user'" json:"role"`
IsActive bool `gorm:"default:true" json:"is_active"`
Posts []Post `gorm:"foreignKey:UserID" json:"posts,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // Soft delete
}
type Post struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"not null" json:"title"`
Content string `gorm:"type:text" json:"content"`
Published bool `gorm:"default:false" json:"published"`
UserID uint `gorm:"not null;index" json:"user_id"`
User *User `json:"user,omitempty"`
Comments []Comment `gorm:"foreignKey:PostID" json:"comments,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
type Comment struct {
ID uint `gorm:"primaryKey" json:"id"`
Content string `gorm:"type:text;not null" json:"content"`
PostID uint `gorm:"not null;index" json:"post_id"`
UserID uint `gorm:"not null;index" json:"user_id"`
User *User `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// BeforeCreate hook example
func (u *User) BeforeCreate(tx *gorm.DB) error {
// Add validation or pre-processing here
return nil
}---
5. Application Setup
cmd/api/main.go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"{{module_path}}/internal/api/routes"
"{{module_path}}/internal/infrastructure/cache"
"{{module_path}}/internal/infrastructure/config"
"{{module_path}}/internal/infrastructure/database"
"{{module_path}}/internal/pkg/logger"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Initialize logger
log := logger.NewLogger(cfg.Logger)
log.Info("Starting application", "env", cfg.App.Env, "version", cfg.App.Version)
// Connect to database
db, err := database.NewDatabase(&cfg.Database)
if err != nil {
log.Fatal("Failed to connect to database", "error", err)
}
defer db.Close()
// Run migrations
if err := db.AutoMigrate(); err != nil {
log.Fatal("Failed to run migrations", "error", err)
}
// Connect to Redis
redis := cache.NewRedisClient(&cfg.Redis)
defer redis.Close()
// Initialize Fiber app
app := fiber.New(fiber.Config{
AppName: cfg.App.Name,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorHandler: errorHandler,
})
// Global middleware
app.Use(recover.New()) // Panic recovery
app.Use(requestid.New()) // Request ID tracking
app.Use(helmet.New()) // Security headers
app.Use(compress.New()) // Response compression
app.Use(cors.New(cors.Config{
AllowOrigins: cfg.CORS.AllowedOrigins[0], // Join with comma
AllowMethods: cfg.CORS.AllowedMethods[0],
AllowHeaders: cfg.CORS.AllowedHeaders[0],
AllowCredentials: true,
}))
app.Use(limiter.New(limiter.Config{
Max: cfg.RateLimit.Requests,
Expiration: cfg.RateLimit.Window,
}))
// Health check
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
"time": time.Now().UTC(),
})
})
// Setup routes
routes.Setup(app, db, redis, cfg, log)
// Graceful shutdown
go func() {
if err := app.Listen(":" + cfg.App.Port); err != nil {
log.Fatal("Failed to start server", "error", err)
}
}()
log.Info("Server started", "port", cfg.App.Port)
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := app.ShutdownWithContext(ctx); err != nil {
log.Error("Server forced to shutdown", "error", err)
}
log.Info("Server exited")
}
func errorHandler(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
message := "Internal Server Error"
if e, ok := err.(*fiber.Error); ok {
code = e.Code
message = e.Message
}
return c.Status(code).JSON(fiber.Map{
"error": fiber.Map{
"code": code,
"message": message,
},
})
}---
6. Authentication Implementation
internal/pkg/auth/jwt.go
package auth
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWTManager struct {
secret string
expiration time.Duration
}
type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func NewJWTManager(secret string, expiration time.Duration) *JWTManager {
return &JWTManager{
secret: secret,
expiration: expiration,
}
}
func (jm *JWTManager) Generate(userID uint, email, role string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jm.expiration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jm.secret))
}
func (jm *JWTManager) Verify(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(jm.secret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}internal/pkg/auth/password.go
package auth
import (
"errors"
"golang.org/x/crypto/bcrypt"
)
const bcryptCost = 12
func HashPassword(password string) (string, error) {
if len(password) < 8 {
return "", errors.New("password must be at least 8 characters")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return "", err
}
return string(hash), nil
}
func VerifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}internal/api/middleware/auth.go
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
"{{module_path}}/internal/pkg/auth"
)
type AuthMiddleware struct {
jwtManager *auth.JWTManager
}
func NewAuthMiddleware(jwtManager *auth.JWTManager) *AuthMiddleware {
return &AuthMiddleware{jwtManager: jwtManager}
}
func (am *AuthMiddleware) Protected() fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Missing authorization header",
})
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid authorization header format",
})
}
claims, err := am.jwtManager.Verify(parts[1])
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired token",
})
}
// Store user info in context
c.Locals("user_id", claims.UserID)
c.Locals("email", claims.Email)
c.Locals("role", claims.Role)
return c.Next()
}
}
func (am *AuthMiddleware) RequireRole(role string) fiber.Handler {
return func(c *fiber.Ctx) error {
userRole := c.Locals("role").(string)
if userRole != role {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Insufficient permissions",
})
}
return c.Next()
}
}---
7. API Routes & Handlers
internal/api/routes/routes.go
package routes
import (
"github.com/gofiber/fiber/v2"
"{{module_path}}/internal/api/handlers"
"{{module_path}}/internal/api/middleware"
"{{module_path}}/internal/domain/repositories"
"{{module_path}}/internal/domain/services"
"{{module_path}}/internal/infrastructure/cache"
"{{module_path}}/internal/infrastructure/config"
"{{module_path}}/internal/infrastructure/database"
"{{module_path}}/internal/pkg/auth"
"{{module_path}}/internal/pkg/logger"
)
func Setup(
app *fiber.App,
db *database.Database,
redis *cache.RedisClient,
cfg *config.Config,
log *logger.Logger,
) {
// Initialize dependencies
jwtManager := auth.NewJWTManager(cfg.JWT.Secret, cfg.JWT.Expiration)
authMiddleware := middleware.NewAuthMiddleware(jwtManager)
// Repositories
userRepo := repositories.NewUserRepository(db)
postRepo := repositories.NewPostRepository(db)
// Services
userService := services.NewUserService(userRepo, jwtManager)
postService := services.NewPostService(postRepo, userRepo)
// Handlers
authHandler := handlers.NewAuthHandler(userService, log)
userHandler := handlers.NewUserHandler(userService, log)
postHandler := handlers.NewPostHandler(postService, log)
// API v1 routes
api := app.Group("/api/v1")
// Public routes
api.Post("/auth/register", authHandler.Register)
api.Post("/auth/login", authHandler.Login)
// Protected routes
users := api.Group("/users", authMiddleware.Protected())
users.Get("/me", userHandler.GetProfile)
users.Put("/me", userHandler.UpdateProfile)
users.Delete("/me", userHandler.DeleteAccount)
// Admin only
users.Get("/", authMiddleware.Protected(), authMiddleware.RequireRole("admin"), userHandler.ListUsers)
// Posts (protected)
posts := api.Group("/posts", authMiddleware.Protected())
posts.Get("/", postHandler.List)
posts.Get("/:id", postHandler.Get)
posts.Post("/", postHandler.Create)
posts.Put("/:id", postHandler.Update)
posts.Delete("/:id", postHandler.Delete)
}internal/api/handlers/auth_handler.go
package handlers
import (
"github.com/gofiber/fiber/v2"
"{{module_path}}/internal/domain/services"
"{{module_path}}/internal/pkg/logger"
)
type AuthHandler struct {
userService *services.UserService
log *logger.Logger
}
func NewAuthHandler(userService *services.UserService, log *logger.Logger) *AuthHandler {
return &AuthHandler{
userService: userService,
log: log,
}
}
type RegisterRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Name string `json:"name" validate:"required,min=2,max=50"`
}
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
func (h *AuthHandler) Register(c *fiber.Ctx) error {
var req RegisterRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
})
}
// Validate request (use validator library in production)
if req.Email == "" || req.Password == "" || req.Name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Missing required fields",
})
}
user, token, err := h.userService.Register(req.Email, req.Password, req.Name)
if err != nil {
h.log.Error("Registration failed", "error", err, "email", req.Email)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
h.log.Info("User registered", "user_id", user.ID, "email", user.Email)
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"user": user,
"token": token,
})
}
func (h *AuthHandler) Login(c *fiber.Ctx) error {
var req LoginRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
})
}
user, token, err := h.userService.Login(req.Email, req.Password)
if err != nil {
h.log.Warn("Login failed", "error", err, "email", req.Email)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid email or password",
})
}
h.log.Info("User logged in", "user_id", user.ID, "email", user.Email)
return c.JSON(fiber.Map{
"user": user,
"token": token,
})
}---
8. Repository Pattern / Data Access
internal/domain/repositories/user_repository.go
package repositories
import (
"errors"
"gorm.io/gorm"
"{{module_path}}/internal/domain/models"
"{{module_path}}/internal/infrastructure/database"
)
type UserRepository interface {
Create(user *models.User) error
FindByID(id uint) (*models.User, error)
FindByEmail(email string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) error
List(offset, limit int) ([]models.User, int64, error)
}
type userRepository struct {
db *database.Database
}
func NewUserRepository(db *database.Database) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
func (r *userRepository) FindByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmail(email string) (*models.User, error) {
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
func (r *userRepository) Update(user *models.User) error {
return r.db.Save(user).Error
}
func (r *userRepository) Delete(id uint) error {
// Soft delete
return r.db.Delete(&models.User{}, id).Error
}
func (r *userRepository) List(offset, limit int) ([]models.User, int64, error) {
var users []models.User
var total int64
if err := r.db.Model(&models.User{}).Count(&total).Error; err != nil {
return nil, 0, err
}
err := r.db.Offset(offset).Limit(limit).Order("created_at DESC").Find(&users).Error
return users, total, err
}internal/domain/services/user_service.go
package services
import (
"errors"
"{{module_path}}/internal/domain/models"
"{{module_path}}/internal/domain/repositories"
"{{module_path}}/internal/pkg/auth"
)
type UserService struct {
repo repositories.UserRepository
jwtManager *auth.JWTManager
}
func NewUserService(repo repositories.UserRepository, jwtManager *auth.JWTManager) *UserService {
return &UserService{
repo: repo,
jwtManager: jwtManager,
}
}
func (s *UserService) Register(email, password, name string) (*models.User, string, error) {
// Check if user already exists
existing, _ := s.repo.FindByEmail(email)
if existing != nil {
return nil, "", errors.New("email already in use")
}
// Hash password
hashedPassword, err := auth.HashPassword(password)
if err != nil {
return nil, "", err
}
// Create user
user := &models.User{
Email: email,
Password: hashedPassword,
Name: name,
Role: "user",
IsActive: true,
}
if err := s.repo.Create(user); err != nil {
return nil, "", err
}
// Generate JWT
token, err := s.jwtManager.Generate(user.ID, user.Email, user.Role)
if err != nil {
return nil, "", err
}
return user, token, nil
}
func (s *UserService) Login(email, password string) (*models.User, string, error) {
user, err := s.repo.FindByEmail(email)
if err != nil {
return nil, "", errors.New("invalid credentials")
}
if err := auth.VerifyPassword(user.Password, password); err != nil {
return nil, "", errors.New("invalid credentials")
}
if !user.IsActive {
return nil, "", errors.New("account is deactivated")
}
token, err := s.jwtManager.Generate(user.ID, user.Email, user.Role)
if err != nil {
return nil, "", err
}
return user, token, nil
}---
9. Testing
tests/unit/services/user_service_test.go
package services_test
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"{{module_path}}/internal/domain/models"
"{{module_path}}/internal/domain/services"
"{{module_path}}/internal/pkg/auth"
)
// Mock repository
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Create(user *models.User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) FindByEmail(email string) (*models.User, error) {
args := m.Called(email)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
// More mock methods...
func TestUserService_Register(t *testing.T) {
mockRepo := new(MockUserRepository)
jwtManager := auth.NewJWTManager("test-secret-key-min-32-characters", 24*time.Hour)
service := services.NewUserService(mockRepo, jwtManager)
t.Run("successful registration", func(t *testing.T) {
mockRepo.On("FindByEmail", "test@example.com").Return(nil, errors.New("not found"))
mockRepo.On("Create", mock.AnythingOfType("*models.User")).Return(nil)
user, token, err := service.Register("test@example.com", "password123", "Test User")
assert.NoError(t, err)
assert.NotNil(t, user)
assert.NotEmpty(t, token)
assert.Equal(t, "test@example.com", user.Email)
mockRepo.AssertExpectations(t)
})
t.Run("duplicate email", func(t *testing.T) {
existingUser := &models.User{Email: "test@example.com"}
mockRepo.On("FindByEmail", "test@example.com").Return(existingUser, nil)
user, token, err := service.Register("test@example.com", "password123", "Test User")
assert.Error(t, err)
assert.Nil(t, user)
assert.Empty(t, token)
assert.Contains(t, err.Error(), "already in use")
})
}tests/integration/api/auth_test.go
package api_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"{{module_path}}/internal/api/handlers"
"{{module_path}}/internal/domain/repositories"
"{{module_path}}/internal/domain/services"
"{{module_path}}/internal/infrastructure/database"
"{{module_path}}/internal/pkg/auth"
"{{module_path}}/internal/pkg/logger"
)
func setupTestApp(t *testing.T) *fiber.App {
// Setup test database
db := setupTestDatabase(t)
// Initialize dependencies
jwtManager := auth.NewJWTManager("test-secret", time.Hour)
userRepo := repositories.NewUserRepository(db)
userService := services.NewUserService(userRepo, jwtManager)
log := logger.NewLogger(logger.Config{Level: "error"})
authHandler := handlers.NewAuthHandler(userService, log)
app := fiber.New()
app.Post("/register", authHandler.Register)
app.Post("/login", authHandler.Login)
return app
}
func TestAuthAPI_Register(t *testing.T) {
app := setupTestApp(t)
payload := map[string]string{
"email": "test@example.com",
"password": "password123",
"name": "Test User",
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusCreated, resp.StatusCode)
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
assert.NotNil(t, result["user"])
assert.NotEmpty(t, result["token"])
}---
10. Docker Setup
Dockerfile (Multi-stage)
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
# Install dependencies
RUN apk add --no-cache git ca-certificates tzdata
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o main ./cmd/api
# Final stage
FROM scratch
# Copy CA certificates and timezone data
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=builder /app/main /main
# Expose port
EXPOSE 8080
# Run
ENTRYPOINT ["/main"]docker-compose.yml
version: '3.9'
services:
api:
build: .
ports:
- "8080:8080"
environment:
- APP_ENV=development
- DATABASE_URL=postgres://postgres:postgres@postgres:5432/myapp?sslmode=disable
- REDIS_URL=redis://redis:6379/0
- JWT_SECRET=your-super-secret-jwt-key-min-32-chars
depends_on:
- postgres
- redis
volumes:
- .:/app
restart: unless-stopped
postgres:
image: postgres:18-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
volumes:
postgres_data:
redis_data:Makefile
.PHONY: run build test migrate-up migrate-down docker-up docker-down
run:
go run cmd/api/main.go
build:
go build -o bin/main cmd/api/main.go
test:
go test -v -race -coverprofile=coverage.out ./...
test-coverage:
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
migrate-create:
migrate create -ext sql -dir migrations -seq $(name)
migrate-up:
migrate -path migrations -database "$(DATABASE_URL)" up
migrate-down:
migrate -path migrations -database "$(DATABASE_URL)" down
docker-up:
docker-compose up -d
docker-down:
docker-compose down
docker-build:
docker-compose build
lint:
golangci-lint run
fmt:
go fmt ./...
tidy:
go mod tidy
air:
air---
11. Production Checklist
Security [OK]
- [ ] JWT secret is strong (min 32 chars) and stored in secret manager
- [ ] HTTPS enforced (TLS 1.2+ only)
- [ ] Rate limiting configured per endpoint
- [ ] CORS restricted to specific origins
- [ ] SQL injection prevention (GORM parameterized queries)
- [ ] Password hashing with bcrypt (cost 12+)
- [ ] Input validation on all endpoints
- [ ] Security headers (Helmet middleware)
- [ ] Dependency vulnerability scanning (go mod audit)
- [ ] API key rotation policy
Performance [OK]
- [ ] Database connection pooling configured
- [ ] Redis caching for read-heavy operations
- [ ] Cursor-based pagination for large datasets
- [ ] Database indexes on foreign keys and query columns
- [ ] Response compression enabled
- [ ] Goroutine pool limits set
- [ ] Profiling enabled (pprof endpoints)
- [ ] Load testing completed (k6, vegeta)
Observability [OK]
- [ ] Structured logging (JSON format)
- [ ] Request ID tracking
- [ ] Error tracking (Sentry, Rollbar)
- [ ] APM integration (New Relic, Datadog)
- [ ] Health check endpoint
- [ ] Metrics exposed (Prometheus format)
- [ ] Distributed tracing (OpenTelemetry)
Deployment [OK]
- [ ] Multi-stage Docker build
- [ ] Container security scanning
- [ ] Environment variables validated
- [ ] Database migrations automated
- [ ] Graceful shutdown implemented
- [ ] Zero-downtime deployment strategy
- [ ] Rollback plan documented
- [ ] CI/CD pipeline configured
Reliability [OK]
- [ ] Database backups automated
- [ ] Redis persistence configured
- [ ] Circuit breaker for external services
- [ ] Retry logic with exponential backoff
- [ ] Timeout configured for all operations
- [ ] Panic recovery middleware
- [ ] Dead letter queue for failed jobs
---
12. API Documentation
Swagger/OpenAPI Setup
// Install: go get -u github.com/swaggo/swag/cmd/swag
// Install: go get -u github.com/gofiber/swagger
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/swagger"
_ "{{module_path}}/docs" // Generated by swag
)
// @title Your API
// @version 1.0
// @description API documentation for your backend service
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.example.com/support
// @contact.email support@example.com
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func main() {
app := fiber.New()
// Swagger route
app.Get("/swagger/*", swagger.HandlerDefault)
// Your routes...
}
// Generate docs: swag init -g cmd/api/main.goExample Documented Endpoint
// Register godoc
// @Summary Register a new user
// @Description Create a new user account
// @Tags auth
// @Accept json
// @Produce json
// @Param request body RegisterRequest true "User registration details"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /auth/register [post]
func (h *AuthHandler) Register(c *fiber.Ctx) error {
// Implementation...
}---
END
Congratulations! You now have a production-grade Go backend with Fiber, GORM, and PostgreSQL.
Next Steps: 1. Run go mod tidy to install dependencies 2. Copy .env.example to .env and configure 3. Start services with docker-compose up -d 4. Run migrations with make migrate-up 5. Start development server with make run or make air (hot reload) 6. Access Swagger docs at http://localhost:8080/swagger/index.html
Go-Specific Best Practices:
- Use goroutines for concurrent operations (with proper sync/error handling)
- Leverage Go's context for timeouts and cancellation
- Profile with pprof (
import _ "net/http/pprof") - Use
errgroupfor coordinated goroutine error handling - Implement graceful shutdown with signal handling
- Use
sync.Poolfor reusing expensive objects - Benchmark performance-critical code with
go test -bench
Backend Engineering - Node.js + Prisma + PostgreSQL Template
Purpose: A comprehensive template for building production-grade REST APIs with Node.js, Prisma ORM, and PostgreSQL.
---
When to Use
Use this template when building:
- REST APIs with Express/Fastify/NestJS
- CRUD applications with PostgreSQL
- Authentication systems
- Multi-tenant SaaS backends
- Microservices
- Admin dashboards and internal tools
---
TEMPLATE STARTS HERE
1. Project Overview
Project Name: [Name]
Description: [Brief description of the API purpose]
Tech Stack:
- [ ] Node.js (v18+ recommended)
- [ ] TypeScript
- [ ] Express.js / Fastify / NestJS
- [ ] Prisma ORM
- [ ] PostgreSQL (v14+)
- [ ] Redis (caching/sessions)
- [ ] BullMQ (background jobs)
Team:
- Owner: [Name]
- Backend Lead: [Name]
- Database Admin: [Name]
Timeline:
- Start: [YYYY-MM-DD]
- MVP: [YYYY-MM-DD]
- Launch: [YYYY-MM-DD]
---
2. Project Structure
project-root/
|-- src/
| |-- api/
| | |-- routes/
| | | |-- index.ts # Route aggregator
| | | |-- auth.routes.ts
| | | |-- users.routes.ts
| | | `-- posts.routes.ts
| | |-- controllers/
| | | |-- auth.controller.ts
| | | |-- users.controller.ts
| | | `-- posts.controller.ts
| | |-- middlewares/
| | | |-- auth.middleware.ts
| | | |-- error.middleware.ts
| | | |-- validation.middleware.ts
| | | `-- rateLimit.middleware.ts
| | `-- validators/
| | |-- auth.schema.ts
| | |-- users.schema.ts
| | `-- posts.schema.ts
| |-- services/
| | |-- auth.service.ts
| | |-- users.service.ts
| | |-- posts.service.ts
| | |-- email.service.ts
| | `-- cache.service.ts
| |-- repositories/
| | |-- users.repository.ts
| | `-- posts.repository.ts
| |-- config/
| | |-- env.ts # Environment variables
| | |-- database.ts # Prisma client
| | |-- redis.ts # Redis client
| | `-- logger.ts # Winston/Pino config
| |-- utils/
| | |-- errors.ts # Custom error classes
| | |-- jwt.ts # Token utilities
| | |-- password.ts # Bcrypt utilities
| | `-- pagination.ts # Pagination helpers
| |-- types/
| | |-- express.d.ts # Express type extensions
| | `-- index.ts # Shared types
| |-- jobs/ # Background job workers
| | `-- email.worker.ts
| |-- app.ts # Express app setup
| `-- server.ts # Server entry point
|-- prisma/
| |-- schema.prisma # Database schema
| |-- migrations/ # Migration files
| `-- seed.ts # Database seeding
|-- tests/
| |-- unit/
| |-- integration/
| `-- e2e/
|-- .env.example
|-- .env
|-- .gitignore
|-- docker-compose.yml
|-- Dockerfile
|-- package.json
|-- tsconfig.json
`-- README.md---
3. Environment Configuration
3.1 .env.example
# Node
NODE_ENV=development
PORT=3000
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# Redis
REDIS_URL=redis://localhost:6379
# JWT
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRES_IN=7d
# Email (optional)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-password
# External APIs (if needed)
API_KEY=your-api-key
# Monitoring (optional)
SENTRY_DSN=your-sentry-dsn3.2 Environment Validation
// src/config/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string(),
});
export const env = envSchema.parse(process.env);Checklist:
- [ ] All required variables defined
- [ ] Validation at startup
- [ ] Separate
.envper environment (dev/staging/prod) - [ ] Never commit
.envto git - [ ] Use secrets manager in production (AWS Secrets Manager, Vault)
---
4. Database Setup (Prisma + PostgreSQL)
4.1 Prisma Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
password String
name String
role Role @default(USER)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
sessions Session[]
@@index([email])
@@index([createdAt])
@@map("users")
}
model Post {
id String @id @default(cuid())
title String
content String
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([published, createdAt])
@@map("posts")
}
model Session {
id String @id @default(cuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([token])
@@index([userId])
@@map("sessions")
}
enum Role {
USER
ADMIN
}4.2 Prisma Client Setup
// src/config/database.ts
import { PrismaClient } from '@prisma/client';
import { env } from './env';
const prisma = new PrismaClient({
log: env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
export { prisma };4.3 Migration Workflow
# Development
npx prisma migrate dev --name init
# Production
npx prisma migrate deploy
# Generate Prisma Client
npx prisma generate
# Open Prisma Studio
npx prisma studioChecklist:
- [ ] Schema follows naming conventions
- [ ] Indexes on foreign keys and query columns
- [ ] Cascade deletes configured
- [ ] Timestamps (createdAt, updatedAt) on all models
- [ ] Connection pooling configured for production
---
5. Application Setup (Express)
5.1 Express App Configuration
// src/app.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import { rateLimit } from 'express-rate-limit';
import routes from './api/routes';
import { errorHandler } from './api/middlewares/error.middleware';
import { requestLogger } from './api/middlewares/logger.middleware';
const app = express();
// Security
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true,
}));
// Parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Compression
app.use(compression());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
// Logging
app.use(requestLogger);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Routes
app.use('/api/v1', routes);
// Error handling
app.use(errorHandler);
export { app };5.2 Server Entry Point
// src/server.ts
import { app } from './app';
import { env } from './config/env';
import { logger } from './config/logger';
import { prisma } from './config/database';
const PORT = env.PORT || 3000;
const server = app.listen(PORT, () => {
logger.info(`Server running on port ${PORT} in ${env.NODE_ENV} mode`);
});
// Graceful shutdown
const shutdown = async () => {
logger.info('Shutting down gracefully...');
server.close(async () => {
await prisma.$disconnect();
process.exit(0);
});
// Force shutdown after 10 seconds
setTimeout(() => {
logger.error('Forced shutdown');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);Checklist:
- [ ] Security headers (Helmet)
- [ ] CORS configured
- [ ] Rate limiting enabled
- [ ] Request logging
- [ ] Health check endpoint
- [ ] Graceful shutdown
---
6. Authentication Implementation
6.1 Auth Service
// src/services/auth.service.ts
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { prisma } from '../config/database';
import { env } from '../config/env';
import { AppError } from '../utils/errors';
class AuthService {
async register(email: string, password: string, name: string) {
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
throw new AppError(409, 'Email already registered');
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: { email, password: hashedPassword, name },
select: { id: true, email: true, name: true, role: true },
});
const token = this.generateToken(user.id, user.role);
return { user, token };
}
async login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
throw new AppError(401, 'Invalid credentials');
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new AppError(401, 'Invalid credentials');
}
if (!user.isActive) {
throw new AppError(403, 'Account is deactivated');
}
const token = this.generateToken(user.id, user.role);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
token,
};
}
private generateToken(userId: string, role: string) {
return jwt.sign(
{ userId, role },
env.JWT_SECRET,
{ expiresIn: env.JWT_EXPIRES_IN }
);
}
}
export const authService = new AuthService();6.2 Auth Middleware
// src/api/middlewares/auth.middleware.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import { prisma } from '../../config/database';
import { env } from '../../config/env';
import { AppError } from '../../utils/errors';
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new AppError(401, 'Authentication required');
}
const payload = jwt.verify(token, env.JWT_SECRET) as {
userId: string;
role: string;
};
const user = await prisma.user.findUnique({
where: { id: payload.userId },
select: { id: true, email: true, name: true, role: true, isActive: true },
});
if (!user || !user.isActive) {
throw new AppError(401, 'Invalid or expired token');
}
req.user = user;
next();
} catch (error) {
next(new AppError(401, 'Invalid or expired token'));
}
};
export const authorize = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return next(new AppError(403, 'Insufficient permissions'));
}
next();
};
};Checklist:
- [ ] Password hashing with bcrypt (12 rounds minimum)
- [ ] JWT token with expiration
- [ ] Refresh token mechanism (if needed)
- [ ] Role-based authorization
- [ ] Secure token storage on client (httpOnly cookies recommended)
---
7. API Routes & Controllers
7.1 User Routes
// src/api/routes/users.routes.ts
import { Router } from 'express';
import { usersController } from '../controllers/users.controller';
import { authenticate, authorize } from '../middlewares/auth.middleware';
import { validate } from '../middlewares/validation.middleware';
import { updateUserSchema } from '../validators/users.schema';
const router = Router();
router.get('/', authenticate, authorize('ADMIN'), usersController.getAll);
router.get('/:id', authenticate, usersController.getById);
router.patch(
'/:id',
authenticate,
validate(updateUserSchema),
usersController.update
);
router.delete('/:id', authenticate, authorize('ADMIN'), usersController.delete);
export default router;7.2 Users Controller
// src/api/controllers/users.controller.ts
import { Request, Response, NextFunction } from 'express';
import { usersService } from '../../services/users.service';
class UsersController {
async getAll(req: Request, res: Response, next: NextFunction) {
try {
const { cursor, limit } = req.query;
const result = await usersService.listUsers({
cursor: cursor as string,
limit: limit ? parseInt(limit as string) : undefined,
});
res.json(result);
} catch (error) {
next(error);
}
}
async getById(req: Request, res: Response, next: NextFunction) {
try {
const user = await usersService.getUserById(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
}
async update(req: Request, res: Response, next: NextFunction) {
try {
const user = await usersService.updateUser(req.params.id, req.body);
res.json(user);
} catch (error) {
next(error);
}
}
async delete(req: Request, res: Response, next: NextFunction) {
try {
await usersService.deleteUser(req.params.id);
res.status(204).send();
} catch (error) {
next(error);
}
}
}
export const usersController = new UsersController();Checklist:
- [ ] Proper HTTP methods (GET, POST, PUT, PATCH, DELETE)
- [ ] Authentication on protected routes
- [ ] Authorization checks
- [ ] Input validation
- [ ] Error handling via next()
---
8. Repository Pattern
// src/repositories/users.repository.ts
import { prisma } from '../config/database';
import { Prisma } from '@prisma/client';
interface PaginationParams {
cursor?: string;
limit?: number;
}
class UsersRepository {
async findMany({ cursor, limit = 20 }: PaginationParams) {
const users = await prisma.user.findMany({
take: limit + 1,
...(cursor && { skip: 1, cursor: { id: cursor } }),
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
},
});
const hasMore = users.length > limit;
const items = hasMore ? users.slice(0, -1) : users;
return {
items,
nextCursor: hasMore ? items[items.length - 1].id : null,
hasMore,
};
}
async findById(id: string) {
return prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
updatedAt: true,
},
});
}
async findByEmail(email: string) {
return prisma.user.findUnique({ where: { email } });
}
async create(data: Prisma.UserCreateInput) {
return prisma.user.create({ data });
}
async update(id: string, data: Prisma.UserUpdateInput) {
return prisma.user.update({ where: { id }, data });
}
async delete(id: string) {
await prisma.user.delete({ where: { id } });
}
}
export const usersRepository = new UsersRepository();Checklist:
- [ ] Repository layer isolates data access
- [ ] Select only needed fields
- [ ] Support pagination
- [ ] Transaction support where needed
---
9. Testing
9.1 Unit Test Example
// tests/unit/services/auth.service.test.ts
import { authService } from '../../../src/services/auth.service';
import { prisma } from '../../../src/config/database';
import bcrypt from 'bcrypt';
jest.mock('../../../src/config/database', () => ({
prisma: {
user: {
findUnique: jest.fn(),
create: jest.fn(),
},
},
}));
describe('AuthService', () => {
describe('register', () => {
it('should create user with hashed password', async () => {
(prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
(prisma.user.create as jest.Mock).mockResolvedValue({
id: '1',
email: 'test@example.com',
name: 'Test User',
role: 'USER',
});
const result = await authService.register(
'test@example.com',
'password123',
'Test User'
);
expect(result.user.email).toBe('test@example.com');
expect(result.token).toBeDefined();
});
it('should throw error if email exists', async () => {
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
id: '1',
email: 'test@example.com',
});
await expect(
authService.register('test@example.com', 'password123', 'Test User')
).rejects.toThrow('Email already registered');
});
});
});9.2 Integration Test Example
// tests/integration/api/auth.test.ts
import request from 'supertest';
import { app } from '../../../src/app';
import { prisma } from '../../../src/config/database';
describe('POST /api/v1/auth/register', () => {
beforeAll(async () => {
// Setup test database
});
afterAll(async () => {
await prisma.$disconnect();
});
it('should register new user', async () => {
const response = await request(app)
.post('/api/v1/auth/register')
.send({
email: 'newuser@example.com',
password: 'password123',
name: 'New User',
})
.expect(201);
expect(response.body.user.email).toBe('newuser@example.com');
expect(response.body.token).toBeDefined();
});
it('should return 409 if email exists', async () => {
// First registration
await request(app).post('/api/v1/auth/register').send({
email: 'duplicate@example.com',
password: 'password123',
name: 'User',
});
// Duplicate registration
const response = await request(app)
.post('/api/v1/auth/register')
.send({
email: 'duplicate@example.com',
password: 'password123',
name: 'User 2',
})
.expect(409);
expect(response.body.message).toBe('Email already registered');
});
});Checklist:
- [ ] Unit tests for services and utilities
- [ ] Integration tests for API endpoints
- [ ] Test database separate from development
- [ ] Mock external dependencies
- [ ] Test both success and error cases
- [ ] 80%+ code coverage
---
10. Docker Setup
10.1 Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY prisma ./prisma/
RUN npm ci
COPY . .
RUN npx prisma generate
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/prisma ./prisma
EXPOSE 3000
CMD ["npm", "start"]10.2 docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@postgres:5432/mydb
- REDIS_URL=redis://redis:6379
- JWT_SECRET=your-secret-key
depends_on:
- postgres
- redis
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:Checklist:
- [ ] Multi-stage build for smaller image
- [ ] Non-root user
- [ ] Health checks
- [ ] Volume mounts for persistence
- [ ] Environment variables
---
11. Production Checklist
11.1 Security
- [ ] HTTPS enabled
- [ ] Security headers (Helmet)
- [ ] Rate limiting
- [ ] CORS configured
- [ ] Input validation
- [ ] SQL injection prevention (Prisma handles this)
- [ ] XSS prevention
- [ ] CSRF protection (if using cookies)
- [ ] Secrets in environment variables/secrets manager
- [ ] Dependency vulnerability scanning
11.2 Performance
- [ ] Database indexes on query columns
- [ ] Connection pooling configured
- [ ] Caching layer (Redis)
- [ ] Response compression
- [ ] Pagination for list endpoints
- [ ] Query optimization (select only needed fields)
11.3 Monitoring
- [ ] Structured logging (Pino/Winston)
- [ ] Error tracking (Sentry)
- [ ] APM (New Relic/Datadog)
- [ ] Health check endpoint
- [ ] Database query monitoring
- [ ] Cache hit ratio monitoring
11.4 Deployment
- [ ] CI/CD pipeline configured
- [ ] Automated tests in pipeline
- [ ] Database migrations automated
- [ ] Environment variables managed
- [ ] Graceful shutdown handling
- [ ] Zero-downtime deployments
- [ ] Rollback plan
---
12. API Documentation
12.1 OpenAPI/Swagger Setup
// src/config/swagger.ts
import swaggerJSDoc from 'swagger-jsdoc';
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API documentation',
},
servers: [
{
url: 'http://localhost:3000/api/v1',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
apis: ['./src/api/routes/*.ts'],
};
export const swaggerSpec = swaggerJSDoc(options);Checklist:
- [ ] API documentation generated
- [ ] All endpoints documented
- [ ] Request/response schemas defined
- [ ] Authentication documented
---
END
This template provides a production-ready foundation for Node.js + Prisma + PostgreSQL backends. Customize based on specific project requirements.
Backend Engineering Template Guide
How to add new tech stack templates to this skill.
Contents
- Overview
- Template Structure
- Sections Explained
- Example: Adding Python + FastAPI Template
- Tech Stack Comparison Matrix
- Required Components
- Quality Checklist
- Naming Convention
- Future Templates
- Contributing
- Shared Utilities (Implementation Patterns)
- Resources
- Questions?
- Version
---
Overview
The software-backend skill is designed to be extensible. You can easily add support for new tech stacks by creating new template files following the established pattern.
---
Template Structure
Each template should follow this structure:
# Backend Engineering - [Tech Stack Name] Template
*Purpose: Brief description of when to use this stack*
---
# When to Use
Use this template when building:
- [Use case 1]
- [Use case 2]
- [Use case 3]
---
# TEMPLATE STARTS HERE
# 1. Project Overview
# 2. Project Structure
# 3. Environment Configuration
# 4. Database Setup
# 5. Application Setup
# 6. Authentication Implementation
# 7. API Routes & Controllers
# 8. Repository Pattern / Data Access
# 9. Testing
# 10. Docker Setup
# 11. Production Checklist
# 12. API Documentation
# END---
Sections Explained
1. Project Overview
- Project name placeholder
- Tech stack components
- Team roles
- Timeline
2. Project Structure
- Directory tree
- File organization
- Module structure
- Naming conventions
3. Environment Configuration
.env.examplefile- Environment validation
- Configuration management
- Secrets handling
4. Database Setup
- Schema definition
- Migration workflow
- ORM/Database client setup
- Connection configuration
5. Application Setup
- Framework initialization
- Middleware configuration
- Security setup
- Server entry point
- Graceful shutdown
6. Authentication Implementation
- Auth service
- Middleware/guards
- Token generation
- Password hashing
- Authorization logic
Implementation Reference: See auth-utilities.md for Argon2id password hashing, jose JWT, and OAuth 2.1/PKCE patterns.
7. API Routes & Controllers
- Route definitions
- Controller implementations
- Request handlers
- Response formatting
8. Repository Pattern / Data Access
- Repository interfaces
- Data access implementations
- Query methods
- Transaction handling
9. Testing
- Unit test examples
- Integration test examples
- E2E test examples
- Test setup
- Coverage configuration
Implementation Reference: See testing-utilities.md for Vitest, MSW v2, factories, and fixtures.
10. Docker Setup
- Dockerfile
- Multi-stage builds
- docker-compose.yml
- Environment configuration
11. Production Checklist
- Security checklist
- Performance checklist
- Monitoring checklist
- Deployment checklist
12. API Documentation
- Documentation setup
- Schema definitions
- Example endpoints
---
Example: Adding Python + FastAPI Template
Step 1: Create Template File
assets/template-python-fastapi-sqlalchemy.md
# Backend Engineering - Python + FastAPI + SQLAlchemy Template
*Purpose: Production-grade Python APIs with FastAPI and PostgreSQL*
---
# When to Use
Use this template when building:
- High-performance Python APIs
- Data science backends
- ML model serving
- Async Python applications
---
# TEMPLATE STARTS HERE
# 1. Project Overview
**Tech Stack:**
- [ ] Python 3.11+
- [ ] FastAPI
- [ ] SQLAlchemy 2.0
- [ ] PostgreSQL 14+
- [ ] Redis (caching)
- [ ] Celery (background jobs)
- [ ] Pytest (testing)
---
# 2. Project Structure
project-root/ |-- app/ | |-- api/ | | |-- routes/ | | |-- dependencies/ | | -- schemas/ | |-- core/ | | |-- config.py | | |-- security.py | | -- database.py | |-- models/ | |-- services/ | |-- repositories/ | -- main.py |-- tests/ |-- alembic/ |-- requirements/ | |-- base.txt | |-- dev.txt | -- prod.txt |-- Dockerfile |-- docker-compose.yml `-- pyproject.toml
[Continue with remaining sections...]Step 2: Update sources.json
Add Python/FastAPI resources:
{
"python_frameworks": [
{
"name": "FastAPI Documentation",
"url": "https://fastapi.tiangolo.com/",
"type": "framework",
"relevance": "Modern Python web framework with automatic API docs, async support",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true
},
{
"name": "SQLAlchemy Documentation",
"url": "https://docs.sqlalchemy.org/",
"type": "orm",
"relevance": "Python ORM, database toolkit, migrations",
"update_frequency": "continuous",
"access": "free",
"add_as_web_search": true
},
{
"name": "Pydantic Documentation",
"url": "https://docs.pydantic.dev/",
"type": "library",
"relevance": "Data validation using Python type annotations",
"update_frequency": "active",
"access": "free",
"add_as_web_search": true
}
],
"python_testing": [
{
"name": "Pytest Documentation",
"url": "https://docs.pytest.org/",
"type": "testing",
"relevance": "Python testing framework, fixtures, mocking",
"update_frequency": "active",
"access": "free",
"add_as_web_search": false
}
]
}Step 3: Update README.md
Add to "Supported Tech Stacks" section:
### Supported Tech Stacks
[OK] **Node.js + Prisma + PostgreSQL** (complete)
[OK] **Python + FastAPI + SQLAlchemy** (complete)Step 4: Reference in skill.md
Update the "Templates" section:
# Templates
See `assets/` directory for tech-stack-specific implementations:
- `template-nodejs-prisma-postgres.md` - Node.js + Prisma + PostgreSQL
- `template-python-fastapi-sqlalchemy.md` - Python + FastAPI + SQLAlchemy
- More templates can be added for other stacks---
Tech Stack Comparison Matrix
When creating a new template, consider this comparison:
| Aspect | Node.js + Prisma | Python + FastAPI | Go + GORM | Ruby on Rails |
|---|---|---|---|---|
| Performance | High (async) | High (async) | Very High | Medium |
| Type Safety | TypeScript | Pydantic | Native | Sorbet |
| Learning Curve | Medium | Low-Medium | Medium-High | Low |
| Ecosystem | npm (huge) | PyPI (large) | Go modules | RubyGems |
| Best For | APIs, real-time | ML/DS backends | Microservices | Full-stack web |
| ORM | Prisma | SQLAlchemy | GORM | ActiveRecord |
| Concurrency | Event loop | asyncio | Goroutines | Threads |
---
Required Components
Every template must include:
Core Patterns [OK]
- [x] Project structure
- [x] Environment configuration
- [x] Database setup and migrations
- [x] Application initialization
- [x] Route/endpoint definitions
- [x] Request/response handling
- [x] Data access layer
Security [OK]
- [x] Authentication implementation
- [x] Authorization/RBAC
- [x] Password hashing
- [x] Input validation
- [x] Security headers
- [x] Rate limiting
- [x] CORS configuration
Performance [OK]
- [x] Database query optimization
- [x] Caching strategy
- [x] Pagination implementation
- [x] Response compression
- [x] Connection pooling
Testing [OK]
- [x] Unit test examples
- [x] Integration test examples
- [x] E2E test examples
- [x] Test database setup
- [x] Mocking strategies
Operations [OK]
- [x] Docker configuration
- [x] docker-compose for local dev
- [x] Health check endpoint
- [x] Graceful shutdown
- [x] Logging configuration
- [x] Error tracking setup
Documentation [OK]
- [x] API documentation setup
- [x] README with setup steps
- [x] Environment variable docs
- [x] Deployment guide
- [x] Production checklist
---
Quality Checklist
Before submitting a new template:
Code Quality
- [ ] All code examples are tested and working
- [ ] Follows language-specific conventions
- [ ] Type safety enabled where available
- [ ] Consistent naming conventions
- [ ] Proper error handling in all examples
Completeness
- [ ] All 12 sections are filled out
- [ ] Real-world code examples provided
- [ ] Configuration files are complete
- [ ] Docker setup is production-ready
- [ ] Tests cover critical paths
Documentation
- [ ] Clear explanations for each pattern
- [ ] "When to Use" guidance provided
- [ ] Checklists for validation
- [ ] Links to official documentation
- [ ] Common pitfalls mentioned
Integration
- [ ] Referenced in README.md
- [ ] Resources added to sources.json
- [ ] Mentioned in skill.md templates section
- [ ] Command file updated (if needed)
---
Naming Convention
Template files should follow this pattern:
template-<language>-<framework>-<database>.mdExamples:
- [OK]
template-nodejs-prisma-postgres.md - [OK]
template-python-fastapi-sqlalchemy.md - [OK]
template-go-fiber-gorm.md - [OK]
template-ruby-rails-postgres.md - [OK]
template-java-spring-jpa.md - [OK]
template-csharp-aspnet-efcore.md
---
Future Templates
Ideas for additional templates:
High Priority
- [ ] Python + FastAPI + SQLAlchemy + PostgreSQL
- [ ] Go + Fiber + GORM + PostgreSQL
- [ ] Java + Spring Boot + JPA + PostgreSQL
- [ ] C# + ASP.NET Core + EF Core + PostgreSQL
Medium Priority
- [ ] Ruby on Rails + PostgreSQL
- [ ] PHP + Laravel + Eloquent + PostgreSQL
- [ ] Rust + Actix + Diesel + PostgreSQL
- [ ] Kotlin + Ktor + Exposed + PostgreSQL
Specialized
- [ ] Node.js + GraphQL + Prisma + PostgreSQL
- [ ] Python + Django REST Framework + PostgreSQL
- [ ] Elixir + Phoenix + Ecto + PostgreSQL
- [ ] Scala + Play + Slick + PostgreSQL
---
Contributing
To contribute a new template:
1. Fork the repository 2. Create a new template file following this guide 3. Update sources.json with relevant resources 4. Update README.md to list the new stack 5. Test all code examples 6. Submit a pull request with:
- Template file
- Updated documentation
- Example project (optional)
---
Shared Utilities (Implementation Patterns)
For cross-cutting implementation concerns, reference these centralized utilities:
- auth-utilities.md - Argon2id password hashing, jose JWT, OAuth 2.1/PKCE
- error-handling.md - Effect Result types, correlation IDs, error boundaries
- config-validation.md - Zod 3.24+, Valibot, secrets management (1Password/Doppler)
- resilience-utilities.md - p-retry v6, opossum v8 circuit breaker, OTel spans
- logging-utilities.md - pino v9 + OpenTelemetry integration
- testing-utilities.md - Vitest, MSW v2, factories, fixtures
- observability-utilities.md - OpenTelemetry SDK, tracing, metrics
---
Resources
- Node.js Template - Reference implementation
- Backend Skill Documentation - Core patterns
- Sources JSON - Resource format
---
Questions?
If you have questions about creating a new template:
1. Review the existing Node.js template 2. Check the skill.md for core patterns 3. Consult sources.json for resource structure 4. Open an issue in the repository
---
Version
- Version: 1.0.0
- Created: 2025-11-17
- Last Updated: 2025-11-17
Edge Deployment Guide - Backend Engineering
Patterns for edge computing, serverless backends, and modern JavaScript runtimes.
Contents
- When to Use Edge Computing
- Platform Decision Matrix
- Runtime Comparison (Verify Versions)
- Edge-First Frameworks
- tRPC: End-to-End Type Safety
- Edge-Compatible Databases
- Zero-Trust Security Patterns
- Observability at Edge
- Migration Strategies
- Related Resources
---
When to Use Edge Computing
| Use Case | Edge | Traditional Server |
|---|---|---|
| Global low-latency APIs | Yes | No |
| Authentication/JWT validation | Yes | Yes |
| A/B testing, feature flags | Yes | Yes |
| Database-heavy CRUD | No | Yes |
| Long-running processes | No | Yes |
| WebSocket connections | Limited | Yes |
Rule of thumb: Edge is ideal for stateless, CPU-light operations close to users.
---
Platform Decision Matrix
| Platform | Cold Start | Global Locations | Best For |
|---|---|---|---|
| Cloudflare Workers | <1ms (V8 isolates) | 300+ | Pure edge logic, APIs |
| Vercel Edge Functions | ~50ms | 20+ | Next.js integration |
| AWS Lambda@Edge | ~100ms | CloudFront POPs | AWS ecosystem |
| Deno Deploy | <10ms | 35+ | Deno/Fresh apps |
| Fly.io | ~50ms | 30+ | Containers at edge |
Cloudflare Workers
Strengths: Fastest cold starts, largest network, generous free tier (100k requests/day).
// Hono on Cloudflare Workers
import { Hono } from 'hono'
const app = new Hono()
app.get('/api/user/:id', async (c) => {
const id = c.req.param('id')
// Use Cloudflare KV or D1 for data
const user = await c.env.USERS_KV.get(id, 'json')
return c.json(user)
})
export default appLimitations: No Node.js APIs, 128MB memory, 30s CPU time (paid), no native WebSockets.
Vercel Edge Functions
Strengths: Seamless Next.js integration, familiar DX, automatic ISR.
// Next.js Edge API Route
export const config = { runtime: 'edge' }
export default function handler(req: Request) {
return new Response(JSON.stringify({ message: 'Hello from edge' }), {
headers: { 'content-type': 'application/json' },
})
}Limitations: Higher latency than Workers, usage-based pricing can escalate.
---
Runtime Comparison (Verify Versions)
| Runtime | TypeScript | Package Manager | Best For |
|---|---|---|---|
| Node.js (current LTS) | Via transpile/runtime loaders | npm/pnpm/yarn | Production stability, broad ecosystem |
| Bun | Native | bun | Perf-sensitive services (verify constraints) |
| Deno | Native | deno add | Security-focused deployments (verify platform support) |
Bun Quick Start
# Install Bun
curl -fsSL https://bun.sh/install | bash
# Create Hono project
bun create hono my-app
cd my-app
bun run dev
# Run tests
bun test
# Bundle for production
bun build ./src/index.ts --outdir ./dist --target bunWhen to Choose Bun
- Greenfield projects with performance requirements
- Serverless functions (often lower cold starts)
- Development tooling (instant installs, fast tests)
- Full-stack TypeScript with Elysia/Hono
When to Stay with Node.js
- Large existing codebases
- Enterprise requirements with compliance audits
- Dependencies with native Node.js bindings
- Need for maximum ecosystem compatibility
Runtime Sandboxing (Prefer Platform Controls)
Prefer OS/container sandboxing (containers, seccomp, gVisor, Firecracker) for untrusted workloads. If your runtime offers a permissions model, follow its official docs and verify flag stability before relying on it in production.
---
Edge-First Frameworks
Hono 4.x
Best for: Cloudflare Workers, multi-runtime deployment.
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { jwt } from 'hono/jwt'
const app = new Hono()
// Middleware
app.use('*', cors())
app.use('/api/*', jwt({ secret: 'your-secret' }))
// Routes
app.get('/api/health', (c) => c.json({ status: 'ok' }))
app.post('/api/users', async (c) => {
const body = await c.req.json()
// Validate with Zod
const user = userSchema.parse(body)
return c.json(user, 201)
})
export default appElysia (Bun-native)
Best for: Maximum performance on Bun, end-to-end type safety.
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/api/health', () => ({ status: 'ok' }))
.post('/api/users', ({ body }) => body, {
body: t.Object({
email: t.String({ format: 'email' }),
name: t.String({ minLength: 2 }),
}),
})
.listen(3000)
console.log(`Running at ${app.server?.hostname}:${app.server?.port}`)---
tRPC: End-to-End Type Safety
Why tRPC in 2026
- No schemas or code generation; rely on TypeScript inference
- Type errors caught at compile time, not runtime
- Ideal for full-stack TypeScript monorepos
- Works with Next.js, Remix, SvelteKit, standalone
Server Setup
// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const router = t.router
export const publicProcedure = t.procedure
// server/routers/user.ts
export const userRouter = router({
getById: publicProcedure
.input(z.string())
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input } })
}),
create: publicProcedure
.input(z.object({
email: z.string().email(),
name: z.string().min(2),
}))
.mutation(async ({ input }) => {
return db.user.create({ data: input })
}),
})Client Usage
// Client automatically infers types from server
const user = await trpc.user.getById.query('user-123')
// ^? User | null (inferred from server return type)
await trpc.user.create.mutate({
email: 'test@example.com',
name: 'Test User',
})
// TypeScript error if fields don't match server schematRPC vs REST vs GraphQL
| Aspect | tRPC | REST | GraphQL |
|---|---|---|---|
| Type Safety | Full (inference) | Manual (OpenAPI) | Partial (codegen) |
| Schema | None needed | OpenAPI/Swagger | SDL required |
| Bundle Size | ~2kb | N/A | ~20kb+ |
| Learning Curve | Low | Low | Medium |
| Public API | Not ideal | Yes | Yes |
Use tRPC when: Full-stack TypeScript, internal APIs, monorepos. Use REST when: Public APIs, multi-language clients, OpenAPI requirement. Use GraphQL when: Complex data fetching, multiple clients with different needs.
---
Edge-Compatible Databases
| Database | Type | Edge Support | Best For |
|---|---|---|---|
| Cloudflare D1 | SQLite | Native | Cloudflare Workers |
| Turso | SQLite (libSQL) | Yes | Multi-region SQLite |
| Neon | PostgreSQL | Yes (HTTP) | Serverless Postgres |
| PlanetScale | MySQL | Yes | Serverless MySQL |
| Upstash Redis | Redis | Yes | Caching, sessions |
Drizzle + Turso Example
import { drizzle } from 'drizzle-orm/libsql'
import { createClient } from '@libsql/client'
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
})
const db = drizzle(client)
// Type-safe queries
const users = await db.select().from(usersTable).where(eq(usersTable.id, '123'))---
Zero-Trust Security Patterns
In 2026, every request is adversarial until proven otherwise.
Authentication at Edge
// JWT validation at edge (Hono)
import { jwt } from 'hono/jwt'
app.use('/api/*', jwt({
secret: process.env.JWT_SECRET!,
cookie: 'token', // Also check cookies
}))
// Access claims in handlers
app.get('/api/me', (c) => {
const payload = c.get('jwtPayload')
return c.json({ userId: payload.sub })
})Rate Limiting at Edge
// Using Cloudflare Workers + Durable Objects
app.use('/api/*', async (c, next) => {
const ip = c.req.header('CF-Connecting-IP')
const rateLimiter = c.env.RATE_LIMITER.get(c.env.RATE_LIMITER.idFromName(ip))
const { allowed } = await rateLimiter.check()
if (!allowed) {
return c.json({ error: 'Rate limited' }, 429)
}
await next()
})Security Checklist
- [ ] Validate JWT at edge before origin
- [ ] Implement rate limiting per IP/user
- [ ] Use httpOnly cookies for tokens
- [ ] Enable CORS with specific origins
- [ ] Sanitize all user input
- [ ] Use Cloudflare WAF or similar
- [ ] Rotate secrets via secret manager
- [ ] Log security events to observability backend
---
Observability at Edge
OpenTelemetry Integration
import { trace } from '@opentelemetry/api'
const tracer = trace.getTracer('edge-api')
app.use('*', async (c, next) => {
const span = tracer.startSpan(`${c.req.method} ${c.req.path}`)
try {
await next()
span.setStatus({ code: 1 }) // OK
} catch (error) {
span.setStatus({ code: 2, message: error.message }) // ERROR
throw error
} finally {
span.end()
}
})Structured Logging
// Edge-compatible logging (no Pino/Winston)
const log = (level: string, message: string, meta?: object) => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level,
message,
...meta,
}))
}
app.use('*', async (c, next) => {
const start = Date.now()
await next()
log('info', 'Request completed', {
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration: Date.now() - start,
})
})---
Migration Strategies
Node.js to Bun
1. Test compatibility: bun run test with existing test suite 2. Check native modules: Replace with pure JS alternatives if needed 3. Update scripts: Change npm run to bun run 4. Deploy gradually: Run Bun in staging before production
Express to Hono
// Express
app.get('/users/:id', async (req, res) => {
const user = await getUser(req.params.id)
res.json(user)
})
// Hono (almost identical)
app.get('/users/:id', async (c) => {
const user = await getUser(c.req.param('id'))
return c.json(user)
})Prisma to Drizzle (for edge)
1. Export Prisma schema to SQL 2. Define Drizzle schema matching tables 3. Migrate queries (Drizzle API mirrors SQL) 4. Test with edge runtime before deploying
---
Related Resources
- SKILL.md - Backend engineering skill overview
- backend-best-practices.md - Template authoring and best practices
- data/sources.json - External references
- ../software-security-appsec/SKILL.md - Security patterns