6.4 KiB
6.4 KiB
HR-ATS Backend LLM Context Prompt
Copy everything below the line into any LLM session before asking it to write or edit backend code.
You are coding inside HR-ATS-Portal (backend/). You must follow this house style exactly. Mirror neighboring files. Do not invent alternate patterns, layers, or response shapes. Prefer matching existing code over “cleaner” industry defaults.
Goal
Every change must look like it was written by the same author as backend/users/ and backend/inbox/.
Package layout (every domain)
backend/<domain>/
app.py # routes only — HTTP in/out
views.py # service class — business logic
models.py # SQLModel table + classmethod DB accessors
serializers.py # hand-rolled dict builders (no Pydantic response models)
plugins.py # pure helpers (hash, JWT, clean payload) — NO FastAPI imports
permissions.py # OAuth2 scheme + Depends aliases (auth domains only)
- Bare
router = APIRouter(); mount inmain.pywithapp.include_router(...). - No package
__init__.py. Run frombackend/so imports are top-level (users.app,db_setup). - Non-DB config: module-level
load_dotenv()+os.getenv(...). Do not extenddb_setup.Settingsfor app secrets.
Layer duties (non-negotiable)
| Layer | Owns | Must NOT do |
|---|---|---|
app.py |
Routes, inline request Pydantic models, JSONResponse, HTTP token envelope via serializers, inject session / CurrentUser |
Business rules, SQL, JWT crypto beyond calling plugin functions |
views.py |
Business checks, call models, raise HTTPException, return ORM user (auth) or serialized dict (CRUD) |
Call serialize_token or build login HTTP payloads |
models.py |
Fields, queries, inserts/updates/soft-delete, selectinload when needed |
HTTPException, FastAPI, serializers |
serializers.py |
serialize_* → plain dict |
DB, Depends |
plugins.py |
Pure helpers; raise library errors (jwt.*) |
Import FastAPI / raise HTTPException |
permissions.py |
OAuth2PasswordBearer, get_current_user, CurrentUser alias, require_permission |
Route handlers |
Exact route pattern (app.py)
- Paths are verb-in-path:
/users/create,/users/fetch,/users/login— not/auth/token, not REST-resource-only. - Standard wrapper on every handler:
try:
service=User(session=session)
data=await service.some_method(...)
return JSONResponse(content={"data":data,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
- List:
{"data":items,"total":total,"status_code":200}. By id: include"total":1. - Login / refresh — service returns ORM user; route mints tokens and serializes:
user=await service.authenticate_user(form_data.username,form_data.password)
tokens=serialize_token(create_access_token(user),create_refresh_token(user),user)
return JSONResponse(content={**tokens,"status_code":200})
- Request body models stay inline in
app.py(UserCreate,UserUpdate,TokenRefresh). Never move them intoserializers.py. - Use
session: AsyncSession = Depends(get_session)by default. UseAnnotatedonly when required (OAuth2PasswordRequestForm,CurrentUserbefore other defaulted params). - Preserve tight local spacing:
service=User(session=session),detail=str(e). Do not pretty-reformat unrelated code.
Exact service pattern (views.py)
class User:
def __init__(self,session:AsyncSession):
self.session=session
async def create_user(self,payload):
...
return serialize_user(user)
- Leave service method parameters untyped (match existing).
- Raise
HTTPException(status_code=...,detail="...")for domain errors. authenticate_user/refresh_access_tokenreturn the Users ORM instance only.- If you loaded via an accessor that does not
selectinload(role), re-fetch withget_user_by_idbefore serialization that touchesuser.role. (get_user_by_emailandget_user_by_idboth eager-loadroletoday.)
Exact model pattern (models.py)
- SQLModel
table=True; accessors as@classmethod async def. - Soft delete sets
is_deleted=Trueandis_active=False. selectinloadrelations that serializers read.- Commits happen inside write accessors (existing convention).
Serializers
- Hand-built dicts only.
str(uuid),.isoformat()for datetimes. Never includepassword. - Token response: OAuth2 fields at root (
access_token,refresh_token,token_type,expires_in); user record underdata.
Auth (when touching users auth)
- PyJWT access + refresh with a
typeclaim;decode_token(..., expected_type=...)rejects mismatches. - Protect
/users/*withcurrent_user: CurrentUserexcept/users/loginand/users/refresh. PreferDepends(require_permission(...))on mutating/list routes that need a specific tag; keep/users/meon plainCurrentUserso users can discover a missing-role state. get_current_user: decode access → DB bysub→ reject missing/deleted/inactive → returnserialize_user(user, with_permissions=True).- Login:
OAuth2PasswordRequestForm(username = email).tokenUrl="users/login"(no leading slash). - JWT
iat/expusedatetime.now(timezone.utc)only — never naivedatetime.now().
Dependencies / env
- Add pins to
backend/requirements.txtunder banner comments with a trailing# whycomment. - Put secrets in
backend/.env; keep key names inbackend/.env.example.
Hard bans
- No repository / use-case / DTO layers beyond inline request models.
- No Pydantic response models; no alternate envelopes; no
/api/v1prefix. - No
serialize_tokeninsideviews.py. - No FastAPI imports in
plugins.py. - No drive-by refactors, renames, or whole-file reformats.
- RBAC exists in
users/permissions.py; do not invent a second scheme. - Do not edit unrelated domains (
inbox/vsusers/) unless asked. - Do not add
__init__.pyto make packages.
Workflow when adding an endpoint
- Model accessor (if DB).
- Service method in
views.py. serialize_*if new shape.- Route in
app.pywith the standard try/except +JSONResponse. - Add
current_user: CurrentUserorDepends(require_permission(...))if the route is protected.
Before finishing, re-read the touched files and confirm they still match a sibling file’s structure, naming, spacing, and response shape.