0% found this document useful (0 votes)
14 views23 pages

Solvera API & Backend Developer Guide

The document outlines the guidelines for API and backend development at Solvera, focusing on standardization for backend engineers, ensuring API consistency, and establishing scalable architecture for SaaS products. It details the system architecture, tech stack specifics, project structure, API design, authentication flow, database access, and deployment workflow. Additionally, it includes standards for testing, quality, security, and Git workflow to maintain code integrity and performance.

Uploaded by

Azqamil Al-Gazza
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
14 views23 pages

Solvera API & Backend Developer Guide

The document outlines the guidelines for API and backend development at Solvera, focusing on standardization for backend engineers, ensuring API consistency, and establishing scalable architecture for SaaS products. It details the system architecture, tech stack specifics, project structure, API design, authentication flow, database access, and deployment workflow. Additionally, it includes standards for testing, quality, security, and Git workflow to maintain code integrity and performance.

Uploaded by

Azqamil Al-Gazza
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

🟧 SOLVERA - API & BACKEND DEVELOPER GUIDELINE

🟩 1. TUJUAN GUIDELINE
Dokumen ini berfungsi untuk:

1.​ Menjadi acuan standar untuk seluruh backend engineer Solvera.​

2.​ Menjamin API konsisten, aman, dan mudah diintegrasikan dengan frontend.​

3.​ Menetapkan arsitektur yang scalable untuk SaaS product (SuperJob, SuperContact, dll).​

4.​ Menetapkan workflow CI/CD, branching, testing, deployment.​

🟩 2. OVERALL SYSTEM ARCHITECTURE


Flow kerja platform:

1.​ Frontend [Link] → mengirim request (REST API / fetch)​

2.​ Backend FastAPI menerima request → validasi → service logic → DB/Odoo​

3.​ Neon PostgreSQL untuk penyimpanan data​

4.​ Odoo Integration Layer untuk master data, finance, atau transaction tertentu​

5.​ Vercel untuk frontend hosting​

6.​ Railway menjalankan full ASGI FastAPI (heavy workloads)​

🟩 3. TECH STACK SPECIFICS


🔹 Backend Framework: FastAPI
●​ Async-friendly​
●​ High performance​

●​ Native OpenAPI docs​

●​ Sangat cocok untuk integrasi Odoo & modern apps​

🔹 Database: Neon PostgreSQL


●​ Serverless PostgreSQL modern​

●​ Connection pooling​

●​ Biasanya 100% cocok untuk FastAPI via SQLAlchemy​

🔹 ORM: SQLAlchemy
●​ Clean model definition​

●​ Mature ecosystem​

●​ Support migrations via Alembic​

🔹 Authentication: BetterAuth (NextAuth / JWT)


●​ Auth dikelola frontend​

●​ Backend hanya melakukan JWT verification​

🔹 Deployment Backend: Railway


●​ Mendukung Gunicorn + Uvicorn worker​

●​ Auto-scaling​

●​ Sangat stabil untuk heavy API​

🔹 ERP Layer (optional per project): Odoo


●​ Integrasi via REST/XML-RPC​

●​ FastAPI bertindak sebagai middleware​


🟩 4. BACKEND PROJECT STRUCTURE (SOLVERA
STANDARD)
backend/
app/
api/
v1/
endpoints/
[Link]
[Link]
[Link]
odoo_sync.py
[Link]
core/
[Link]
[Link]
[Link]
db/
[Link]
[Link]
migrations/
models/
[Link]
[Link]
schemas/
user_schema.py
job_schema.py
services/
user_service.py
job_service.py
odoo_service.py
integrations/
odoo_rpc.py
odoo_rest.py
utils/
[Link]
[Link]
tests/
Dockerfile
[Link]
[Link]
.env

Alur:​
Endpoint → Service → Repository/Model → DB / Odoo → Response

🟩 5. API DESIGN GUIDELINE (SOLVERA STANDARD)


⭐ Versioning
/api/v1/... wajib digunakan.

⭐ Naming (RESTful)
●​ List: GET /jobs​

●​ Create: POST /jobs​

●​ Detail: GET /jobs/{id}​

●​ Update: PUT /jobs/{id}​

●​ Delete: DELETE /jobs/{id}​

⭐ Response Format (Standard Solvera)


Success

{
"success": true,
"data": {
"id": 1,
"title": "Backend Dev"
},
"error": null
}

Error
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Title is required",
"details": {}
}
}

⭐ Error Codes Solvera


Error Code Description

VALIDATION_ERROR Data request salah

AUTH_REQUIRED Token tidak ditemukan

FORBIDDEN Role tidak cukup

NOT_FOUND Data tidak ditemukan

INTEGRATION_ERRO Error komunikasi dengan Odoo


R

SERVER_ERROR Unexpected

🟩 6. DATA SCHEMA & VALIDATION (PYDANTIC)


Contoh Schema:

class JobCreate(BaseModel):
title: str
description: str
location: str

class JobOut(JobCreate):
id: int
class Config:
orm_mode = True

Semua request wajib melewati schema validation.


🟩 7. AUTHENTICATION FLOW (BETTER AUTH / NEXTAUTH)
Frontend ([Link]) → login → NextAuth menghasilkan JWT session:

Authorization: Bearer <jwt_token>

Backend FastAPI (verifikasi token):

from jose import jwt

def get_current_user(token: str = Depends(oauth2_scheme)):


payload = [Link](token, settings.JWT_SECRET, algorithms=["HS256"])
return UserSession(id=payload["sub"], role=payload["role"])

🟩 8. DATABASE ACCESS LAYER (NEON + SQLALCHEMY)


Startup
engine = create_engine(settings.DATABASE_URL)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)

Example model
class Job(Base):
__tablename__ = "jobs"

id = Column(Integer, primary_key=True)
title = Column(String)
description = Column(Text)
location = Column(String)

🟩 9. ODOO INTEGRATION (RPC / REST)


RPC (standard Odoo)
import [Link]
url = settings.ODOO_URL
common = [Link](f"{url}/xmlrpc/2/common")
uid = [Link](settings.ODOO_DB, settings.ODOO_USER,
settings.ODOO_PASSWORD, {})

models = [Link](f"{url}/xmlrpc/2/object")

partners = models.execute_kw(
settings.ODOO_DB, uid, settings.ODOO_PASSWORD,
'[Link]', 'search_read',
[[]], {'fields': ['name', 'email']}
)

REST (Modern)
response = [Link](
f"{settings.ODOO_URL}/api/v1/partners",
headers={"Authorization": f"Bearer {settings.ODOO_TOKEN}"}
)

Semua integrasi Odoo diisolasi ke dalam folder:​


/integrations/odoo_rpc.py

🟩 10. DEPLOYMENT WORKFLOW — RAILWAY


Dockerfile
FROM python:3.12-slim

WORKDIR /app
COPY [Link] .
RUN pip install -r [Link]
COPY . .

CMD ["gunicorn", "[Link]:app", "-k", "[Link]",


"--workers", "4", "--bind", "[Link]:8000"]
Deploy
railway login
railway init
railway up

Runtime
●​ Stable​

●​ No serverless limitation​

●​ Autoscaling available​

🟩 11. FRONTEND ↔ BACKEND CONTRACT


Frontend [Link]:

const res = await fetch(`${[Link].NEXT_PUBLIC_API}/jobs`, {


method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${[Link]}`
},
body: [Link](form)
})

Backend FastAPI:

●​ Validasi token​

●​ Proses business logic​

●​ Akses DB / Odoo​

●​ Kirim response JSON standar​


🟩 12. GIT WORKFLOW (SOLVERA STANDARD)
Branch Naming

●​ feature/superjob-create-job​

●​ fix/supercontact-duplicate-contact​

●​ refactor/api-auth-handler​

Commit Convention

●​ feat: add create job endpoint​

●​ fix: handle odoo timeout​

●​ refactor: optimize job service​

●​ chore: update requirements​

Merge Request Template

●​ What changed​

●​ Endpoints updated​

●​ DB migration​

●​ Testing evidence​

●​ Breaking change? (yes/no)​

🟩 13. TESTING STANDARD (API & BACKEND)


●​ Unit test (pytest)​

●​ Integration test (DB + API)​


●​ Postman/Thunder Client testing​

●​ Load testing (Locust)​

●​ Odoo integration testing​

Folder:

/tests
test_jobs.py
test_users.py
test_odoo_integration.py

🟩 14. QUALITY & SECURITY CHECKLIST


✔ Security

●​ JWT validation mandatory​

●​ No exposed sensitive data​

●​ Strict CORS​

●​ Rate limiting for sensitive endpoints​

✔ Database

●​ All models have index​

●​ No nullable field tanpa alasan​

●​ Migration selalu up-to-date​

✔ Code Quality

●​ Async I/O where possible​

●​ Logging structured​

●​ Single-responsibility each service​


🗂 Struktur Folder (bagian yang terkait Jobs)
backend/
app/
[Link]
core/
[Link]
[Link]
[Link]
db/
[Link]
[Link]
models/
[Link]
[Link] # hanya untuk contoh auth
schemas/
job_schema.py
user_schema.py
services/
job_service.py
odoo_job_sync.py
integrations/
odoo_client.py
api/
v1/
endpoints/
[Link]
[Link]
[Link]

1️⃣ Konfigurasi Dasar

app/core/[Link]
from pydantic import BaseSettings

class Settings(BaseSettings):
PROJECT_NAME: str = "Solvera Backend"
API_V1_PREFIX: str = "/api/v1"
DATABASE_URL: str

JWT_SECRET: str
JWT_ALGORITHM: str = "HS256"

ODOO_URL: str | None = None


ODOO_DB: str | None = None
ODOO_USER: str | None = None
ODOO_PASSWORD: str | None = None

class Config:
env_file = ".env"

settings = Settings()

app/db/[Link]
from sqlalchemy import create_engine
from [Link] import sessionmaker
from [Link] import settings

engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
db = SessionLocal()
try:
yield db
finally:
[Link]()

app/db/[Link]
from [Link] import declarative_base

Base = declarative_base()
2️⃣ Model & Schema “Job”

app/models/[Link]
from sqlalchemy import Column, Integer, String, Text, Enum, DateTime, func
from [Link] import Base
import enum

class JobStatus(str, [Link]):


open = "open"
closed = "closed"
draft = "draft"

class Job(Base):
__tablename__ = "jobs"

id = Column(Integer, primary_key=True, index=True)


title = Column(String(255), nullable=False)
description = Column(Text, nullable=False)
location = Column(String(255), nullable=True)
status = Column(Enum(JobStatus), nullable=False, default=[Link])
created_at = Column(DateTime(timezone=True), server_default=[Link]())
updated_at = Column(
DateTime(timezone=True),
server_default=[Link](),
onupdate=[Link](),
)

app/schemas/job_schema.py
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
from [Link] import JobStatus

class JobBase(BaseModel):
title: str = Field(..., min_length=3, max_length=255)
description: str = Field(..., min_length=10)
location: Optional[str] = Field(None, max_length=255)
status: JobStatus = [Link]

class JobCreate(JobBase):
pass

class JobUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=3, max_length=255)
description: Optional[str] = Field(None, min_length=10)
location: Optional[str] = Field(None, max_length=255)
status: Optional[JobStatus] = None

class JobOut(JobBase):
id: int
created_at: datetime
updated_at: datetime

class Config:
orm_mode = True

class PaginatedJobs(BaseModel):
items: List[JobOut]
total: int
page: int
limit: int

3️⃣ Security & Auth

app/core/[Link] (verifikasi JWT dari NextAuth/BetterAuth)


from fastapi import HTTPException, status, Depends
from [Link] import OAuth2PasswordBearer
from jose import jwt, JWTError
from pydantic import BaseModel
from [Link] import settings
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # tidak dipakai
langsung

class CurrentUser(BaseModel):
id: int
email: str
role: str = "user"

def get_current_user(token: str = Depends(oauth2_scheme)) -> CurrentUser:


if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="AUTH_REQUIRED",
)
try:
payload = [Link](token, settings.JWT_SECRET,
algorithms=[settings.JWT_ALGORITHM])
return CurrentUser(
id=[Link]("sub"),
email=[Link]("email"),
role=[Link]("role", "user"),
)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="INVALID_TOKEN",
)

4️⃣ Integrasi Odoo (opsional, tapi contoh kita panggil saat


create/update)

app/integrations/odoo_client.py
import [Link]
from [Link] import settings
def get_odoo_client():
if not settings.ODOO_URL:
return None, None, None

common = [Link](f"{settings.ODOO_URL}/xmlrpc/2/common")
uid = [Link](
settings.ODOO_DB,
settings.ODOO_USER,
settings.ODOO_PASSWORD,
{},
)

models = [Link](f"{settings.ODOO_URL}/xmlrpc/2/object")
return common, uid, models

app/services/odoo_job_sync.py
from [Link].odoo_client import get_odoo_client
from [Link] import Job

def sync_job_to_odoo(job: Job):


common, uid, models = get_odoo_client()
if not uid:
# Odoo tidak dikonfigurasi → skip
return

data = {
"name": [Link],
"x_job_location": [Link],
"x_job_status": [Link],
"x_job_description": [Link],
}

# Contoh: model custom di Odoo: x_hr_job


models.execute_kw(
[Link],
uid,
[Link],
"x_hr_job",
"create_or_update_from_api", # misal ada custom method
[data],
)

(Implementasi method di Odoo tergantung modul custom Anda; ini hanya hook.)

5️⃣ Service Layer “job_service”

app/services/job_service.py
from [Link] import Session
from typing import Tuple, List
from [Link] import Job
from [Link].job_schema import JobCreate, JobUpdate
from [Link].odoo_job_sync import sync_job_to_odoo
from fastapi import HTTPException, status

def create_job(db: Session, job_in: JobCreate, current_user_id: int) -> Job:


job = Job(
title=job_in.title,
description=job_in.description,
location=job_in.location,
status=job_in.status,
)
[Link](job)
[Link]()
[Link](job)

# optional sync ke Odoo


sync_job_to_odoo(job)

return job

def get_job(db: Session, job_id: int) -> Job:


job = [Link](Job).filter([Link] == job_id).first()
if not job:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="JOB_NOT_FOUND",
)
return job

def list_jobs(
db: Session,
page: int = 1,
limit: int = 20,
status_filter: str | None = None,
) -> Tuple[List[Job], int]:
query = [Link](Job)

if status_filter:
query = [Link]([Link] == status_filter)

total = [Link]()
items = (
query.order_by(Job.created_at.desc())
.offset((page - 1) * limit)
.limit(limit)
.all()
)

return items, total

def update_job(
db: Session,
job_id: int,
job_in: JobUpdate,
current_user_id: int,
) -> Job:
job = get_job(db, job_id)

for field, value in job_in.dict(exclude_unset=True).items():


setattr(job, field, value)

[Link](job)
[Link]()
[Link](job)

sync_job_to_odoo(job)
return job

def delete_job(db: Session, job_id: int, current_user_id: int):


job = get_job(db, job_id)
[Link](job)
[Link]()
return True

6️⃣ Endpoint FastAPI (Router Jobs)

app/api/v1/endpoints/[Link]
from fastapi import APIRouter, Depends, Query, status
from [Link] import Session
from typing import Optional

from [Link] import get_db


from [Link] import get_current_user, CurrentUser
from [Link].job_schema import (
JobCreate,
JobUpdate,
JobOut,
PaginatedJobs,
)
from [Link] import job_service

router = APIRouter(prefix="/jobs", tags=["Jobs"])

@[Link](
"/",
response_model=JobOut,
status_code=status.HTTP_201_CREATED,
)
def create_job_endpoint(
job_in: JobCreate,
db: Session = Depends(get_db),
current_user: CurrentUser = Depends(get_current_user),
):
job = job_service.create_job(db, job_in, current_user.id)
return job

@[Link](
"/",
response_model=PaginatedJobs,
)
def list_jobs_endpoint(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
status_filter: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: CurrentUser = Depends(get_current_user),
):
items, total = job_service.list_jobs(db, page=page, limit=limit,
status_filter=status_filter)
return PaginatedJobs(
items=items,
total=total,
page=page,
limit=limit,
)

@[Link](
"/{job_id}",
response_model=JobOut,
)
def get_job_endpoint(
job_id: int,
db: Session = Depends(get_db),
current_user: CurrentUser = Depends(get_current_user),
):
job = job_service.get_job(db, job_id)
return job

@[Link](
"/{job_id}",
response_model=JobOut,
)
def update_job_endpoint(
job_id: int,
job_in: JobUpdate,
db: Session = Depends(get_db),
current_user: CurrentUser = Depends(get_current_user),
):
job = job_service.update_job(db, job_id, job_in, current_user.id)
return job

@[Link](
"/{job_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
def delete_job_endpoint(
job_id: int,
db: Session = Depends(get_db),
current_user: CurrentUser = Depends(get_current_user),
):
job_service.delete_job(db, job_id, current_user.id)
return None

7️⃣ API Router & Main

app/api/v1/[Link]
from fastapi import APIRouter
from [Link] import jobs

api_router = APIRouter()
api_router.include_router([Link])

app/[Link]
from fastapi import FastAPI
from [Link] import CORSMiddleware

from [Link] import api_router


from [Link] import settings
app = FastAPI(
title=settings.PROJECT_NAME,
version="1.0.0",
)

# Sesuaikan origins dengan domain [Link] di Vercel


app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # TODO: ganti ke domain tertentu di production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(api_router, prefix=settings.API_V1_PREFIX)

@[Link]("/")
def health():
return {"status": "ok", "service": "solvera-backend"}

8️⃣ Contoh Pemanggilan dari [Link] (Frontend)


// contoh di [Link] (App Router)
async function createJob(data: any, token: string) {
const res = await fetch(`${[Link].NEXT_PUBLIC_API_URL}/api/v1/jobs`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: [Link](data),
});

if (![Link]) {
const err = await [Link]();
throw new Error([Link]?.message ?? "Failed to create job");
}

return [Link]();
}
9️⃣ Minimal Migration (Alembic) – Gambaran
Di Alembic, migrasinya kurang lebih:

def upgrade() -> None:


op.create_table(
"jobs",
[Link]("id", [Link], primary_key=True),
[Link]("title", [Link](length=255), nullable=False),
[Link]("description", [Link], nullable=False),
[Link]("location", [Link](length=255), nullable=True),
[Link]("status", [Link]("open", "closed", "draft",
name="jobstatus"), nullable=False),
[Link]("created_at", [Link](timezone=True),
server_default=[Link]()),
[Link]("updated_at", [Link](timezone=True),
server_default=[Link]()),
)

You might also like