
Oma Backend
- 130 installs
- 41 repo stars
- Updated August 4, 2026
- gracefullight/stock-checker
Implement stock-checker OMA backend services, endpoints, persistence, and integrations that power inventory queries and alert logic.
About
Guides implementation of the gracefullight stock-checker OMA backend, including API design, data models, provider integrations, and server patterns needed to fetch, store, and serve inventory status reliably.
- OMA service layout
- REST or RPC endpoints
- Persistence patterns
- Stock data providers
- Alert and query logic
Oma Backend by the numbers
- 130 all-time installs (skills.sh)
- Ranked #2,738 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/gracefullight/stock-checker --skill oma-backendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 130 |
|---|---|
| repo stars | ★ 41 |
| Last updated | August 4, 2026 |
| Repository | gracefullight/stock-checker ↗ |
What it does
Implement stock-checker OMA backend services, endpoints, persistence, and integrations that power inventory queries and alert logic.
Files
Backend Agent - API & Server Specialist
Scheduling
Goal
Implement or review backend APIs, authentication, database integration, server-side business logic, and migrations using the project's existing backend stack and clean architecture boundaries.
Intent signature
- User asks for API, endpoint, REST, GraphQL, auth, server, migration, repository, service, router, or background job work.
- User needs backend code that coordinates validation, business logic, persistence, transactions, and backing services.
When to use
- Building REST APIs or GraphQL endpoints
- Database design and migrations
- Authentication and authorization
- Server-side business logic
- Background jobs and queues
When NOT to use
- Frontend UI -> use Frontend Agent
- Mobile-specific code -> use Mobile Agent
Expected inputs
- Target feature, endpoint, migration, auth flow, or server behavior
- Existing backend stack files such as manifests, routes, services, models, and database config
- API contracts, schemas, validation rules, and persistence requirements
- Required verification commands or project conventions
Expected outputs
- Backend code changes in router, service, repository, model, migration, or test files
- Validated inputs, safe queries, transaction boundaries, and error handling
- Verification results from the execution checklist
Dependencies
- Project stack manifests and existing backend conventions
resources/execution-protocol.md,resources/checklist.md, andresources/orm-reference.md- Optional
stack/stack.yaml,stack/tech-stack.md, snippets, and API templates - Database, queue, cache, mail, auth, or external API resources configured through environment or secret managers
Control-flow features
- Branches by detected stack, ORM/query pattern, auth requirement, migration impact, and transaction scope
- Reads and writes codebase files
- May touch local database migrations or generated code
- Must not hardcode secrets or share unsafe ORM lifecycle objects across concurrent work
Structural Flow
Entry
1. Detect the backend stack from project files first. 2. Identify affected router, service, repository, model, migration, and test boundaries. 3. Load stack-specific references only when needed.
Scenes
1. PREPARE: Determine stack, architecture boundaries, and acceptance criteria. 2. ACQUIRE: Read existing routes, services, repositories, models, schemas, and config. 3. ACT: Implement backend changes with validation, business logic, persistence, and tests. 4. VERIFY: Run relevant lint, type, test, migration, and checklist commands. 5. FINALIZE: Report changed behavior, verification, and unresolved risks.
Transitions
- If stack files exist, follow them before generic guidance.
- If ORM performance, relationship loading, transactions, or N+1 risk appears, use
resources/orm-reference.md. - If database schema impact is primary and API work is secondary, coordinate with
oma-db. - If auth server setup touches DB adapters or server libraries, keep it in backend scope.
Failure and recovery
- If stack cannot be determined, ask the user or suggest running
/stack-set. - If verification fails, fix root cause before handoff.
- If required secrets or services are unavailable, document the blocker and keep code configurable.
Exit
- Success: backend change is implemented, tested, and aligned with local architecture.
- Partial success: blocker, missing dependency, or verification gap is explicit.
Logical Operations
Actions
| Action | SSL primitive | Evidence |
|---|---|---|
| Detect stack and conventions | READ | Manifests, stack files, existing code |
| Select implementation boundary | SELECT | Router/service/repository pattern |
| Validate inputs and schemas | VALIDATE | Stack validation library |
| Implement business logic | WRITE | Service layer code |
| Implement persistence | WRITE | Repository/model/migration code |
| Call external/backing services | CALL_TOOL | DB, queue, cache, auth, or API clients |
| Run verification | CALL_TOOL | Tests, typecheck, lint, migrations |
| Report result | NOTIFY | Final summary |
Tools and instruments
- Project language/framework toolchain
- ORM or database client
- Test, lint, typecheck, and migration commands
- Stack-specific templates and snippets when present
Canonical workflow path
rg --files
rg "route|router|service|repository|model|schema|migration" .Then run the project's discovered verification commands, usually lint/typecheck/tests and migrations when schema changes are involved. Prefer stack/stack.yaml verify: commands when present.
Resource scope
| Scope | Resource target |
|---|---|
CODEBASE | Backend source, tests, schemas, migrations |
LOCAL_FS | Stack references and generated artifacts |
PROCESS | Test, lint, typecheck, migration commands |
CREDENTIALS | Environment-managed DB URLs, API keys, secrets |
NETWORK | External APIs or backing services when required |
Preconditions
- Target behavior and affected backend boundary are identifiable.
- Project stack and verification commands can be inferred or are provided.
- Required credentials remain outside source code.
Effects and side effects
- Mutates backend source files, tests, and possibly migrations.
- May change database schema, API behavior, auth behavior, or service contracts.
- May require generated clients or migration artifacts.
Guardrails
1. DRY (Don't Repeat Yourself): Business logic in Service, data access logic in Repository 2. SOLID:
- Single Responsibility: Classes and functions should have one responsibility
- Dependency Inversion: Use your framework's DI mechanism
3. KISS: Keep it simple and clear
Architecture Pattern
Router (HTTP) → Service (Business Logic) → Repository (Data Access) → ModelsRepository Layer
- Encapsulate DB CRUD and query logic
- No business logic, return ORM entities
Service Layer
- Business logic, Repository composition, external API calls
- Business decisions only here
Router Layer
- Receive HTTP requests, input validation, call Service, return response
- No business logic, inject Service via DI
Core Rules
1. Clean architecture: router → service → repository → models 2. No business logic in route handlers 3. All inputs validated with your stack's validation library 4. Parameterized queries only (never string interpolation) 5. JWT + bcrypt for auth; rate limit auth endpoints 6. Async where supported; type annotations on all signatures 7. Custom exceptions via centralized error module (not raw HTTP exceptions) 8. Explicit ORM loading strategy: do not rely on default relation loading when query shape matters 9. Explicit transaction boundaries: group one business operation into one request/service-scoped unit of work 10. Safe ORM lifecycle: do not share mutable ORM session/entity manager/client objects across concurrent work unless the ORM explicitly supports it 11. Config from environment: DB URLs, API keys, secrets, and feature flags come from env vars or secret managers; never hardcode in source 12. Stateless services: no in-memory session or user state between requests; use external stores (DB, Redis, cache) for shared state 13. Backing services as resources: DB, queue, cache, mail are swappable attached resources connected via config; Repository layer must not assume a specific instance
Stack Detection
1. Project files first: Read existing code, package manifests (pyproject.toml, package.json, Cargo.toml, go.mod, pom.xml, etc.) to determine the tech stack 2. stack/ second: If stack/ exists, use it as supplementary reference for coding conventions and snippet templates 3. Neither exists: Ask the user or suggest running /stack-set
Stack-Specific Reference
- Stack manifest (SSOT):
stack/stack.yaml: structured declaration (language,framework,orm) andverify:contract consumed byoma verify backend. Schema:variants/stack.schema.json. - Tech stack narrative:
stack/tech-stack.md: human-readable reference only;stack.yamlwins on conflict. - Code snippets (copy-paste ready):
stack/snippets.md - API template:
stack/api-template.*
References
Follow resources/execution-protocol.md step by step. See resources/examples.md for input/output examples. Use resources/orm-reference.md when the task involves ORM query performance, relationship loading, transactions, session/client lifecycle, or N+1 analysis. Before submitting, run resources/checklist.md. Vendor-specific execution protocols are injected automatically by oma agent:spawn. Source files live under ../_shared/runtime/execution-protocols/{vendor}.md.
- Execution steps:
resources/execution-protocol.md - Code examples:
resources/examples.md - Checklist:
resources/checklist.md - ORM reference:
resources/orm-reference.md - Error recovery:
resources/error-playbook.md - Context loading:
../_shared/core/context-loading.md - Reasoning templates:
../_shared/core/reasoning-templates.md - Clarification:
../_shared/core/clarification-protocol.md - Context budget:
../_shared/core/context-budget.md - Lessons learned:
../_shared/core/lessons-learned.md - Observability handoff:
../oma-observability/SKILL.md§Integrations — propagators/baggage, span conventions, log correlation, PII redaction
Backend Agent - Self-Verification Checklist
Run through every item before submitting your work.
API Design
- [ ] RESTful conventions followed (proper HTTP methods, status codes)
- [ ] OpenAPI documentation complete (all endpoints documented)
- [ ] Request/response schemas defined with validation library
- [ ] Pagination for list endpoints returning > 20 items
- [ ] Consistent error response format
Database
- [ ] Migrations created and tested
- [ ] Indexes on foreign keys and frequently queried columns
- [ ] No N+1 queries; relation loading strategy chosen explicitly for the ORM in use
- [ ] No over-fetching; selected only required fields/columns/attributes
- [ ] Transactions used for multi-step operations with explicit unit-of-work boundaries
- [ ] ORM session/client/entity-manager lifecycle matches the framework's concurrency model
- [ ] Query risks reviewed: missing indexes, full scans, repeated identical queries, join row multiplication
Security
- [ ] JWT authentication on protected endpoints
- [ ] Password hashing with bcrypt (cost 10-12)
- [ ] Rate limiting on auth endpoints
- [ ] Input validation enforced (no raw user input in queries)
- [ ] SQL injection protected (ORM or parameterized queries)
- [ ] No secrets in code or logs
Testing
- [ ] Unit tests for service layer logic
- [ ] Integration tests for all endpoints (happy + error paths)
- [ ] Auth scenarios tested (missing token, expired, wrong role)
- [ ] Test coverage > 80%
Code Quality
- [ ] Clean architecture layers: router -> service -> repository
- [ ] No business logic in route handlers
- [ ] Async/await used consistently
- [ ] Type annotations on all function signatures
Cloud Readiness
- [ ] No hardcoded config values (DB URLs, API keys, ports); all from env vars
- [ ] No in-process state between requests (sessions, caches, counters)
- [ ] Logs written to stdout/stderr, not file; structured format (JSON) preferred
- [ ] Graceful shutdown handled for background jobs and open connections
Backend Agent - Error Recovery Playbook
When you encounter a failure, find the matching scenario and follow the recovery steps. Do NOT stop or ask for help until you have exhausted the playbook.
---
Import / Module Not Found
Symptoms: Module/package not found errors
1. Check the import path: typo? wrong package name? 2. Verify the dependency exists in your package manifest 3. If missing: note it in your result as "requires install the missing dependency"; do NOT install yourself 4. If it's a local module: check the directory structure with get_symbols_overview 5. If the path changed: use search_for_pattern("class ClassName") to find the new location
---
Test Failure
Symptoms: test runner returns FAILED, assertion errors
1. Read the full error output: which test, which assertion, expected vs actual 2. find_symbol("test_function_name") to read the test code 3. Determine: is the test wrong or is the implementation wrong?
- Test expects old behavior → update test
- Implementation has a bug → fix implementation
4. Run the specific failing test with verbose output 5. After fix, run full test suite to check for regressions 6. After 3 failures: Try a different approach. Record current attempt in progress and implement alternative
---
Database Migration Error
Symptoms: Migration command fails, IntegrityError, duplicate column
1. Read the error; is it a conflict with existing migration? 2. Check current DB state: Check current migration state 3. If migration conflicts: Rollback one migration step then fix migration script 4. If schema mismatch: compare model with actual DB schema 5. NEVER do this: Force-mark migrations as applied (risk of data loss)
---
Authentication / JWT Error
Symptoms: 401/403 responses, InvalidTokenError, ExpiredSignatureError
1. Check: is the secret key consistent between encode and decode? 2. Check: is the algorithm specified (HS256 vs RS256)? 3. Check: is the token being sent in the correct header format? (Bearer {token}) 4. Check: is token expiry set correctly? (access: 15min, refresh: 7day) 5. Test with a manually created token to isolate the issue
---
N+1 Query / Slow Response
Symptoms: API response > 500ms, many similar SQL queries in logs
1. Enable SQL logging on the database connection 2. Count queries for a single request 3. If N+1: add eager loading strategy appropriate for your ORM to the query 4. If slow single query: check indexes with EXPLAIN ANALYZE 5. If still slow: consider caching with Redis
---
Rate Limit / Quota Error (Gemini API)
Symptoms: 429, RESOURCE_EXHAUSTED, rate limit exceeded
1. Stop immediately; do not make additional API calls 2. Save current work to progress-{agent-id}[-{sessionId}].md 3. Record Status: quota_exceeded in result-{agent-id}[-{sessionId}].md 4. Specify remaining tasks so orchestrator can retry later
---
Serena Memory Unavailable
Symptoms: write_memory / read_memory failure, timeout
1. Retry once (may be transient error) 2. If 2 consecutive failures: fall back to local files
- progress → write to
/tmp/progress-{agent-id}[-{sessionId}].md - result → write to
/tmp/result-{agent-id}[-{sessionId}].md
3. Add memory_fallback: true flag to result
---
General Principles
- After 3 failures: If same approach fails 3 times, must try a different method
- Blocked: If no progress after 5 turns, save current state and record
Status: blockedin result - Out of scope: If you find issues in another agent's domain, only record in result; do not modify directly
Backend Agent - Examples
Example 1: CRUD API Endpoint
Input: "Create a TODO CRUD API with user ownership"
Output:
## Task: TODO CRUD API
### Endpoints Implemented
- POST /api/todos (create)
- GET /api/todos (list, user-scoped)
- GET /api/todos/{id} (detail)
- PATCH /api/todos/{id} (update)
- DELETE /api/todos/{id} (delete)
### Database Schema
- todos table: id (UUID), title (VARCHAR), completed (BOOL), user_id (FK), created_at
- Index on user_id
### Security
- [x] All endpoints require JWT auth
- [x] User can only access own todos (user_id filter)
- [x] Input validated with schema validation
### Files Created
- app/models/todo.*
- app/schemas/todo.*
- app/services/todo_service.*
- app/api/todos.*
- tests/test_todos.*
- migrations/xxx_add_todos.*Example 2: Authentication System
Input: "Add JWT authentication with register and login"
Output:
## Task: JWT Authentication
### Endpoints Implemented
- POST /api/auth/register (email + password)
- POST /api/auth/login (returns access + refresh tokens)
- POST /api/auth/refresh (refresh token rotation)
### Security
- [x] Password hashed with bcrypt (cost 12)
- [x] Access token: 15min expiry
- [x] Refresh token: 7 day expiry, single-use rotation
- [x] Rate limiting: 5 attempts/min on login
- [x] Email validation enforced
### Files Created
- app/models/user.*
- app/schemas/auth.*
- app/services/auth_service.*
- app/api/auth.*
- app/core/security.* (JWT helpers, password hashing)
- tests/test_auth.*Example 3: Fixing N+1 Query
Input: "The /api/posts endpoint is slow, taking 2s for 50 posts"
Output:
## Task: Fix N+1 Query in Posts API
### Root Cause
Each post triggers a separate query for its author (N+1 problem).
### Fix
Added eager loading for the Post.author relation.
### Before: 51 queries (1 + 50)
### After: 1 query with JOIN
### Files Modified
- app/api/posts.* (added eager loading)
- tests/test_posts.* (added performance assertion)Backend Agent - Execution Protocol
Step 0: Prepare
1. Assess difficulty: see ../../_shared/core/difficulty-guide.md
- Simple: Skip to Step 3 | Medium: All 4 steps | Complex: All steps + checkpoints
2. Check lessons: read your domain section in ../../_shared/core/lessons-learned.md 3. Clarify requirements: follow ../../_shared/core/clarification-protocol.md
- Check Uncertainty Triggers: business logic, security/auth, existing code conflicts?
- Determine level: LOW → proceed | MEDIUM → present options | HIGH → ask immediately
4. Budget context: follow ../../_shared/core/context-budget.md (read symbols, not whole files)
Intelligent Escalation: When uncertain, escalate early. Don't blindly proceed.
Follow these steps in order (adjust depth by difficulty).
Step 1: Analyze
- Read the task requirements carefully
- Identify which endpoints, models, and services are needed
- Check existing code with Serena:
get_symbols_overview("app/api"),find_symbol("existing_function") - If the task is ORM-heavy, load
resources/orm-reference.mdbefore deciding on loading strategy, transaction scope, or client/session lifecycle - List assumptions; ask if unclear
Step 2: Plan
- Decide on file structure: models, schemas, routes, services
- Define API contracts (method, path, request/response types)
- Plan database schema changes (tables, columns, indexes, migrations)
- Plan relation loading strategy, transaction boundary, and ORM lifecycle constraints explicitly
- Identify security requirements (auth, validation, rate limiting)
Step 3: Implement
- Create/modify files in this order:
1. Database models + migrations 2. Validation schemas (request/response) 3. Service layer (business logic) 4. API routes (thin, delegate to services) 5. Tests (unit + integration)
- Use
stack/api-template.*as reference - Follow clean architecture: router -> service -> repository -> models
Step 4: Verify
- Run
resources/checklist.mditems - Run
../../_shared/core/common-checklist.mditems - Ensure all tests pass
- Confirm OpenAPI docs are complete
On Error
See resources/error-playbook.md for recovery steps.
Backend Agent - ORM Reference
Use this guide when the task involves ORM query design, relation loading, transaction scoping, or session/client lifecycle decisions.
This document is intentionally synthesized from official ORM docs. The purpose is not to mirror each vendor page, but to extract the common operating rules that keep backend work correct and performant.
ORM Rules
1. Relation loading must be chosen per use case
Backend Agent should therefore:
- choose loading strategy per endpoint or service method
- compare join-based eager loading, batched/select-in loading, and separate follow-up queries
- treat accidental lazy loads in loops as a performance bug
What this rule is derived from:
- Prisma documents N+1 mitigation through batched access patterns and join-based relation loading
- SQLAlchemy documents
joinedload,selectinload,lazyload, andraiseloadas explicit strategies - TypeORM documents join-based loading and the lazy/eager trade-off
- Sequelize documents
includeas join-based eager loading andseparate: trueforhasMany - Hibernate documents that select-based fetching is vulnerable to N+1
2. Transaction boundaries must follow business operations
Backend Agent should therefore:
- open one explicit transaction for one business operation
- keep read-modify-write flows inside the same transaction boundary
- avoid per-statement auto-commit designs
- never hold a database transaction open across user think time
What this rule is derived from:
- Hibernate explicitly calls session-per-operation and auto-commit-per-statement anti-patterns
- Sequelize promotes managed transactions around a callback-shaped unit of work
- SQLAlchemy models the session as transaction-oriented state
3. ORM lifecycle objects are not generic shared singletons
Backend Agent should therefore:
- reuse factory/client objects only when the ORM recommends reuse
- keep transaction-scoped mutable objects request-scoped or task-scoped
- never share stateful unit-of-work objects across concurrent tasks unless the vendor explicitly permits it
Operational reading:
- Prisma: reuse one
PrismaClientin long-running apps; do not create a new client per query; do not disconnect after every request - SQLAlchemy:
SessionandAsyncSessionare mutable, transaction-scoped, and the documented model is session-per-thread / async-session-per-task - Hibernate: prefer session-per-request over session-per-operation
4. Field projection is mandatory unless full entity hydration is required
Backend Agent should therefore:
- fetch only required columns, attributes, or nested fields
- prefer projections, DTO shaping, or raw results when domain objects are not needed
- treat over-fetching as a query design flaw, not a minor optimization item
What this rule is derived from:
- Prisma calls out over-fetching directly and supports nested
select - TypeORM recommends
selectandgetRawMany() - Sequelize documents
attributesand{ raw: true }
5. Mapping defaults must never be trusted blindly
Lazy and eager defaults are convenience features, not query plans. Multiple vendors warn that both directions can fail differently:
- lazy defaults can explode into N+1
- eager defaults can create oversized joins, duplicate parent rows, or unnecessary payloads
Backend Agent should therefore:
- inspect relation defaults before changing repository or endpoint behavior
- override defaults at query time when access shape differs from mapping defaults
- prefer repository-level query shaping over relying on entity metadata alone
6. Query review must include access-path and row-shape checks
Backend Agent should therefore review:
- missing indexes on filters, joins, and foreign keys
- full table scans
- repeated identical queries that should be cached or batched
- row multiplication from
hasMany/ collection eager joins - memory cost of hydrating full entities when raw results or projections are enough
"""
API Endpoint Template for Backend Agent
This template demonstrates best practices for FastAPI endpoints.
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Annotated, List
from uuid import UUID
from app.database import get_db
from app.auth import get_current_user
from app.models import User, Resource
from app.schemas import ResourceCreate, ResourceUpdate, ResourceResponse
from app.services import ResourceService
# Type aliases for cleaner code
DatabaseDep = Annotated[Session, Depends(get_db)]
UserDep = Annotated[User, Depends(get_current_user)]
# Router setup
router = APIRouter(
prefix="/api/resources",
tags=["resources"],
responses={404: {"description": "Not found"}},
)
# List endpoint with pagination and filtering
@router.get(
"/",
response_model=List[ResourceResponse],
summary="List resources",
description="Retrieve a paginated list of resources with optional filtering"
)
async def list_resources(
db: DatabaseDep,
current_user: UserDep,
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Max records to return"),
search: str | None = Query(None, description="Search query"),
status: str | None = Query(None, description="Filter by status"),
):
"""
List resources with pagination.
- **skip**: Offset for pagination
- **limit**: Maximum number of records
- **search**: Optional search term
- **status**: Optional status filter
"""
service = ResourceService(db)
resources = service.list_resources(
user_id=current_user.id,
skip=skip,
limit=limit,
search=search,
status=status,
)
return resources
# Get single resource
@router.get(
"/{resource_id}",
response_model=ResourceResponse,
summary="Get resource",
responses={
200: {"description": "Resource found"},
404: {"description": "Resource not found"},
403: {"description": "Access denied"}
}
)
async def get_resource(
resource_id: UUID,
db: DatabaseDep,
current_user: UserDep,
):
"""
Get a specific resource by ID.
Raises:
404: Resource not found
403: User doesn't own this resource
"""
service = ResourceService(db)
resource = service.get_resource(resource_id)
if not resource:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Resource {resource_id} not found"
)
# Authorization check
if resource.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
return resource
# Create resource
@router.post(
"/",
response_model=ResourceResponse,
status_code=status.HTTP_201_CREATED,
summary="Create resource",
)
async def create_resource(
resource_data: ResourceCreate,
db: DatabaseDep,
current_user: UserDep,
):
"""
Create a new resource.
- **name**: Resource name (required)
- **description**: Optional description
- **status**: Initial status (default: active)
"""
service = ResourceService(db)
try:
resource = service.create_resource(
user_id=current_user.id,
data=resource_data
)
return resource
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Update resource
@router.patch(
"/{resource_id}",
response_model=ResourceResponse,
summary="Update resource",
)
async def update_resource(
resource_id: UUID,
resource_data: ResourceUpdate,
db: DatabaseDep,
current_user: UserDep,
):
"""
Update an existing resource (partial update).
Only provided fields will be updated.
"""
service = ResourceService(db)
resource = service.get_resource(resource_id)
if not resource:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Resource {resource_id} not found"
)
if resource.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
try:
updated_resource = service.update_resource(resource, resource_data)
return updated_resource
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Delete resource
@router.delete(
"/{resource_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete resource",
)
async def delete_resource(
resource_id: UUID,
db: DatabaseDep,
current_user: UserDep,
hard: bool = Query(False, description="Perform hard delete instead of soft delete"),
):
"""
Delete a resource.
- **hard=false**: Soft delete (default) - sets deleted_at timestamp
- **hard=true**: Hard delete - permanently removes from database
"""
service = ResourceService(db)
resource = service.get_resource(resource_id)
if not resource:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Resource {resource_id} not found"
)
if resource.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
service.delete_resource(resource, hard=hard)
# 204 No Content - no response body
# Bulk operations example
@router.post(
"/bulk",
response_model=List[ResourceResponse],
status_code=status.HTTP_201_CREATED,
summary="Bulk create resources",
)
async def bulk_create_resources(
resources_data: List[ResourceCreate],
db: DatabaseDep,
current_user: UserDep,
):
"""
Create multiple resources in one request.
Useful for batch imports.
"""
if len(resources_data) > 100:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Maximum 100 resources per bulk operation"
)
service = ResourceService(db)
created_resources = []
try:
for data in resources_data:
resource = service.create_resource(
user_id=current_user.id,
data=data
)
created_resources.append(resource)
return created_resources
except ValueError as e:
db.rollback() # Rollback on error
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Service class template (separate file: app/services/resource_service.py)
"""
from sqlalchemy.orm import Session
from app.models import Resource
from app.schemas import ResourceCreate, ResourceUpdate
from datetime import datetime
from uuid import UUID
class ResourceService:
def __init__(self, db: Session):
self.db = db
def list_resources(
self,
user_id: UUID,
skip: int = 0,
limit: int = 100,
search: str | None = None,
status: str | None = None,
):
query = self.db.query(Resource).filter(
Resource.user_id == user_id,
Resource.deleted_at.is_(None)
)
if search:
query = query.filter(Resource.name.ilike(f"%{search}%"))
if status:
query = query.filter(Resource.status == status)
return query.offset(skip).limit(limit).all()
def get_resource(self, resource_id: UUID) -> Resource | None:
return self.db.query(Resource).filter(
Resource.id == resource_id,
Resource.deleted_at.is_(None)
).first()
def create_resource(self, user_id: UUID, data: ResourceCreate) -> Resource:
resource = Resource(
**data.model_dump(),
user_id=user_id
)
self.db.add(resource)
self.db.commit()
self.db.refresh(resource)
return resource
def update_resource(self, resource: Resource, data: ResourceUpdate) -> Resource:
for field, value in data.model_dump(exclude_unset=True).items():
setattr(resource, field, value)
self.db.commit()
self.db.refresh(resource)
return resource
def delete_resource(self, resource: Resource, hard: bool = False):
if hard:
self.db.delete(resource)
else:
resource.deleted_at = datetime.utcnow()
self.db.commit()
"""
/**
* API Endpoint Template for Backend Agent (Node.js / NestJS)
*
* Demonstrates the Router -> Service -> Repository pattern
* using NestJS, Prisma, and Zod validation.
*
* File layout (split into real modules in production):
* src/modules/resources/
* resources.controller.ts <- this file (controller + decorators)
* resources.service.ts <- business logic
* resources.repository.ts <- Prisma data access
* dto/resource.dto.ts <- Zod schemas + inferred types
* resources.module.ts <- DI wiring
*/
// ---------------------------------------------------------------------------
// dto/resource.dto.ts
// ---------------------------------------------------------------------------
import { z } from 'zod';
export const CreateResourceSchema = z.object({
title: z.string().min(1, 'Title is required').max(200),
description: z.string().max(1000).optional(),
status: z.enum(['active', 'archived']).default('active'),
});
export const UpdateResourceSchema = CreateResourceSchema.partial();
export const ResourceQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
status: z.enum(['active', 'archived']).optional(),
});
export type CreateResourceDto = z.infer<typeof CreateResourceSchema>;
export type UpdateResourceDto = z.infer<typeof UpdateResourceSchema>;
export type ResourceQueryDto = z.infer<typeof ResourceQuerySchema>;
// ---------------------------------------------------------------------------
// resources.repository.ts
// ---------------------------------------------------------------------------
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { Prisma, Resource } from '@prisma/client';
export interface PaginatedResult<T> {
items: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
@Injectable()
export class ResourcesRepository {
constructor(private readonly prisma: PrismaService) {}
async findPaginated(
userId: string,
query: ResourceQueryDto,
): Promise<PaginatedResult<Resource>> {
const { page, limit, search, status } = query;
const where: Prisma.ResourceWhereInput = {
userId,
deletedAt: null,
...(status && { status }),
...(search && {
title: { contains: search, mode: 'insensitive' },
}),
};
// Single round-trip with $transaction
const [items, total] = await this.prisma.$transaction([
this.prisma.resource.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
}),
this.prisma.resource.count({ where }),
]);
return {
items,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: string, userId: string): Promise<Resource | null> {
return this.prisma.resource.findFirst({
where: { id, userId, deletedAt: null },
});
}
async create(
userId: string,
data: CreateResourceDto,
): Promise<Resource> {
return this.prisma.resource.create({
data: {
...data,
user: { connect: { id: userId } },
},
});
}
async update(id: string, data: UpdateResourceDto): Promise<Resource> {
return this.prisma.resource.update({
where: { id },
data,
});
}
async softDelete(id: string): Promise<Resource> {
return this.prisma.resource.update({
where: { id },
data: { deletedAt: new Date() },
});
}
}
// ---------------------------------------------------------------------------
// resources.service.ts
// ---------------------------------------------------------------------------
import {
Injectable,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
@Injectable()
export class ResourcesService {
constructor(private readonly resourcesRepo: ResourcesRepository) {}
async findAll(
userId: string,
query: ResourceQueryDto,
): Promise<PaginatedResult<Resource>> {
return this.resourcesRepo.findPaginated(userId, query);
}
async findOne(id: string, userId: string): Promise<Resource> {
const resource = await this.resourcesRepo.findOne(id, userId);
if (!resource) {
throw new NotFoundException(`Resource ${id} not found`);
}
// Guard against cross-user access (belt-and-suspenders; repo already filters)
if (resource.userId !== userId) {
throw new ForbiddenException('Access denied');
}
return resource;
}
async create(dto: CreateResourceDto, userId: string): Promise<Resource> {
return this.resourcesRepo.create(userId, dto);
}
async update(
id: string,
dto: UpdateResourceDto,
userId: string,
): Promise<Resource> {
await this.findOne(id, userId); // ownership check raises 404/403 as needed
return this.resourcesRepo.update(id, dto);
}
async remove(id: string, userId: string): Promise<void> {
await this.findOne(id, userId); // ownership check
await this.resourcesRepo.softDelete(id);
}
}
// ---------------------------------------------------------------------------
// common/decorators/current-user.decorator.ts
// ---------------------------------------------------------------------------
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '@prisma/client';
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): User => {
const request = ctx.switchToHttp().getRequest<{ user: User }>();
return request.user;
},
);
// ---------------------------------------------------------------------------
// resources.controller.ts (Router layer: NO business logic here)
// ---------------------------------------------------------------------------
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
ParseUUIDPipe,
HttpCode,
HttpStatus,
UseGuards,
UsePipes,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe';
@Controller('api/resources')
@UseGuards(JwtAuthGuard)
export class ResourcesController {
constructor(private readonly resourcesService: ResourcesService) {}
/**
* GET /api/resources?page=1&limit=20&search=foo&status=active
*/
@Get()
findAll(
@Query(new ZodValidationPipe(ResourceQuerySchema)) query: ResourceQueryDto,
@CurrentUser() user: User,
): Promise<PaginatedResult<Resource>> {
return this.resourcesService.findAll(user.id, query);
}
/**
* GET /api/resources/:id
*/
@Get(':id')
findOne(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: User,
): Promise<Resource> {
return this.resourcesService.findOne(id, user.id);
}
/**
* POST /api/resources
*/
@Post()
@HttpCode(HttpStatus.CREATED)
@UsePipes(new ZodValidationPipe(CreateResourceSchema))
create(
@Body() dto: CreateResourceDto,
@CurrentUser() user: User,
): Promise<Resource> {
return this.resourcesService.create(dto, user.id);
}
/**
* PATCH /api/resources/:id
*/
@Patch(':id')
update(
@Param('id', ParseUUIDPipe) id: string,
@Body(new ZodValidationPipe(UpdateResourceSchema)) dto: UpdateResourceDto,
@CurrentUser() user: User,
): Promise<Resource> {
return this.resourcesService.update(id, dto, user.id);
}
/**
* DELETE /api/resources/:id (soft delete)
*/
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: User,
): Promise<void> {
return this.resourcesService.remove(id, user.id);
}
}
// ---------------------------------------------------------------------------
// resources.module.ts
// ---------------------------------------------------------------------------
import { Module } from '@nestjs/common';
import { PrismaModule } from '../../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [ResourcesController],
providers: [ResourcesService, ResourcesRepository],
exports: [ResourcesService],
})
export class ResourcesModule {}
// ---------------------------------------------------------------------------
// common/pipes/zod-validation.pipe.ts
// ---------------------------------------------------------------------------
import { PipeTransform, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private readonly schema: ZodSchema) {}
transform(value: unknown): unknown {
const result = this.schema.safeParse(value);
if (!result.success) {
throw new BadRequestException({
message: 'Validation failed',
errors: result.error.flatten(),
});
}
return result.data;
}
}
Backend Agent - Code Snippets (Node.js / NestJS)
Copy-paste ready patterns. Use these as starting points, adapt to the specific task.
---
1. NestJS Controller with Auth Guard
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
ParseUUIDPipe,
HttpCode,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { ResourcesService } from './resources.service';
import { CreateResourceDto } from './dto/create-resource.dto';
import { UpdateResourceDto } from './dto/update-resource.dto';
import { User } from '@prisma/client';
@Controller('api/resources')
@UseGuards(JwtAuthGuard)
export class ResourcesController {
constructor(private readonly resourcesService: ResourcesService) {}
@Get()
findAll(@CurrentUser() user: User) {
return this.resourcesService.findAll(user.id);
}
@Get(':id')
findOne(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: User,
) {
return this.resourcesService.findOne(id, user.id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(
@Body() dto: CreateResourceDto,
@CurrentUser() user: User,
) {
return this.resourcesService.create(dto, user.id);
}
@Patch(':id')
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateResourceDto,
@CurrentUser() user: User,
) {
return this.resourcesService.update(id, dto, user.id);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: User,
) {
return this.resourcesService.remove(id, user.id);
}
}---
2. Zod Schema Validation
import { z } from 'zod';
// Schema definitions
export const CreateResourceSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
status: z.enum(['active', 'archived']).default('active'),
});
export const UpdateResourceSchema = CreateResourceSchema.partial();
export const PaginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
});
// Inferred TypeScript types
export type CreateResourceDto = z.infer<typeof CreateResourceSchema>;
export type UpdateResourceDto = z.infer<typeof UpdateResourceSchema>;
export type PaginationDto = z.infer<typeof PaginationSchema>;
// NestJS pipe for Zod validation
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
@Injectable()
export class ZodValidationPipe implements PipeTransform {
constructor(private readonly schema: ZodSchema) {}
transform(value: unknown) {
const result = this.schema.safeParse(value);
if (!result.success) {
throw new BadRequestException(result.error.flatten());
}
return result.data;
}
}---
3. Prisma Model Example
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid()) @db.Uuid
email String @unique
password String
resources Resource[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
model Resource {
id String @id @default(uuid()) @db.Uuid
title String @db.VarChar(200)
description String? @db.Text
status String @default("active") @db.VarChar(20)
userId String @map("user_id") @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([userId])
@@map("resources")
}---
4. NestJS Module with DI
import { Module } from '@nestjs/common';
import { ResourcesController } from './resources.controller';
import { ResourcesService } from './resources.service';
import { ResourcesRepository } from './resources.repository';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [ResourcesController],
providers: [ResourcesService, ResourcesRepository],
exports: [ResourcesService],
})
export class ResourcesModule {}
// prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}---
5. Repository/Service Pattern
// resources.repository.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma, Resource } from '@prisma/client';
@Injectable()
export class ResourcesRepository {
constructor(private readonly prisma: PrismaService) {}
async findMany(
where: Prisma.ResourceWhereInput,
options?: { skip?: number; take?: number },
): Promise<Resource[]> {
return this.prisma.resource.findMany({
where,
skip: options?.skip,
take: options?.take,
orderBy: { createdAt: 'desc' },
});
}
async findOne(id: string, userId: string): Promise<Resource | null> {
return this.prisma.resource.findFirst({
where: { id, userId, deletedAt: null },
});
}
async create(data: Prisma.ResourceCreateInput): Promise<Resource> {
return this.prisma.resource.create({ data });
}
async update(id: string, data: Prisma.ResourceUpdateInput): Promise<Resource> {
return this.prisma.resource.update({ where: { id }, data });
}
async softDelete(id: string): Promise<Resource> {
return this.prisma.resource.update({
where: { id },
data: { deletedAt: new Date() },
});
}
}
// resources.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { ResourcesRepository } from './resources.repository';
import { CreateResourceDto, UpdateResourceDto } from './dto/resource.dto';
import { Resource } from '@prisma/client';
@Injectable()
export class ResourcesService {
constructor(private readonly resourcesRepo: ResourcesRepository) {}
async findAll(userId: string): Promise<Resource[]> {
return this.resourcesRepo.findMany({ userId, deletedAt: null });
}
async findOne(id: string, userId: string): Promise<Resource> {
const resource = await this.resourcesRepo.findOne(id, userId);
if (!resource) throw new NotFoundException(`Resource ${id} not found`);
if (resource.userId !== userId) throw new ForbiddenException('Access denied');
return resource;
}
async create(dto: CreateResourceDto, userId: string): Promise<Resource> {
return this.resourcesRepo.create({ ...dto, user: { connect: { id: userId } } });
}
async update(id: string, dto: UpdateResourceDto, userId: string): Promise<Resource> {
await this.findOne(id, userId); // ownership check
return this.resourcesRepo.update(id, dto);
}
async remove(id: string, userId: string): Promise<void> {
await this.findOne(id, userId); // ownership check
await this.resourcesRepo.softDelete(id);
}
}---
6. Paginated Query with Prisma
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Resource } from '@prisma/client';
export interface PaginatedResult<T> {
items: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
@Injectable()
export class ResourcesRepository {
constructor(private readonly prisma: PrismaService) {}
async findPaginated(
userId: string,
page: number = 1,
limit: number = 20,
search?: string,
): Promise<PaginatedResult<Resource>> {
const where = {
userId,
deletedAt: null,
...(search && {
title: { contains: search, mode: 'insensitive' as const },
}),
};
const [items, total] = await this.prisma.$transaction([
this.prisma.resource.findMany({
where,
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
}),
this.prisma.resource.count({ where }),
]);
return {
items,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
}---
7. Prisma Migration Example
# Generate and apply a new migration
npx prisma migrate dev --name add_resources_table
# Apply migrations in production
npx prisma migrate deploy
# Reset database (dev only)
npx prisma migrate reset-- Migration file: prisma/migrations/20240101000000_add_resources_table/migration.sql
CREATE TABLE "resources" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"title" VARCHAR(200) NOT NULL,
"description" TEXT,
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
"user_id" UUID NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "resources_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "resources_user_id_idx" ON "resources"("user_id");
ALTER TABLE "resources"
ADD CONSTRAINT "resources_user_id_fkey"
FOREIGN KEY ("user_id")
REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;---
8. Vitest + Supertest Endpoint Test
// resources.e2e-spec.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/prisma/prisma.service';
describe('Resources (e2e)', () => {
let app: INestApplication;
let prisma: PrismaService;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
prisma = app.get(PrismaService);
// Create a test user and obtain JWT
const loginRes = await request(app.getHttpServer())
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
authToken = loginRes.body.accessToken;
});
afterAll(async () => {
await prisma.resource.deleteMany({ where: { title: { startsWith: 'Test' } } });
await app.close();
});
it('POST /api/resources - creates a resource', async () => {
const res = await request(app.getHttpServer())
.post('/api/resources')
.set('Authorization', `Bearer ${authToken}`)
.send({ title: 'Test Resource', description: 'Test description' });
expect(res.status).toBe(201);
expect(res.body).toMatchObject({
id: expect.any(String),
title: 'Test Resource',
description: 'Test description',
status: 'active',
});
});
it('GET /api/resources - lists resources', async () => {
const res = await request(app.getHttpServer())
.get('/api/resources')
.set('Authorization', `Bearer ${authToken}`);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
it('GET /api/resources/:id - returns 404 for unknown id', async () => {
const res = await request(app.getHttpServer())
.get('/api/resources/00000000-0000-0000-0000-000000000000')
.set('Authorization', `Bearer ${authToken}`);
expect(res.status).toBe(404);
});
it('GET /api/resources - returns 401 without token', async () => {
const res = await request(app.getHttpServer()).get('/api/resources');
expect(res.status).toBe(401);
});
});language: node
source: preset
Backend Agent - Tech Stack Reference (Node.js)
Primary Stack
- Runtime: Node.js 22+ / Bun 1.2+
- Language: TypeScript 5.x (strict mode)
- Framework: NestJS 11+ or Hono 4+
- ORM: Prisma 6+ or Drizzle ORM
- Validation: Zod
- Database: PostgreSQL 16+, Redis 7+
- Auth: jsonwebtoken, bcrypt
- Testing: Vitest, Supertest
- Migrations: Prisma Migrate or Drizzle Kit
Architecture
src/
modules/ # Feature modules (NestJS) or routes (Hono)
common/ # Shared guards, pipes, interceptors
prisma/ # Prisma client and schemaSecurity Requirements
- Password hashing: bcrypt (cost factor 10-12)
- JWT: 15min access tokens, 7 day refresh tokens
- Rate limiting on auth endpoints
- Input validation with Zod schemas
- Parameterized queries via ORM (never raw string interpolation)
Linter/Formatter
- ESLint: with @typescript-eslint
- Prettier: consistent formatting
- Biome: alternative all-in-one (lint + format)