Internal development guide. Code standards, conventions, and workflow.
# 1. Clone and install
git clone <repo-url>
cd Lunex && yarn install
# 2. Environment
cp spot-api/.env.example spot-api/.env
# Fill: DATABASE_URL, REDIS_URL, LUNES_WS_URL
# 3. Database
cd spot-api && npx prisma migrate dev
# 4. Start all services
cd .. && docker-compose -f docker-compose.dev.yml up -d db redis
cd spot-api && yarn dev
cd lunes-dex-main && yarn dev- Strict mode enabled — no
anyexcept documented exceptions - Use
unknownin catch blocks:catch (err: unknown) - Prefer
typeoverinterfacefor plain data shapes - Use Prisma generated types — never cast to
anyfor DB results
Backend (Express routes):
// ✅ Correct — delegate to centralized errorHandler
router.get('/resource', async (req, res, next) => {
try {
const data = await service.getData()
res.json(data)
} catch (err) { next(err) }
})
// ❌ Wrong — inline error responses bypass logging
router.get('/resource', async (req, res) => {
try {
const data = await service.getData()
res.json(data)
} catch (err) {
res.status(500).json({ error: 'Something went wrong' }) // don't do this
}
})Frontend:
// ✅ Correct
} catch (err: unknown) {
setError((err as Error).message || 'Operation failed')
}
// ❌ Wrong
} catch (err: any) {
setError(err.message)
}// ✅ Correct — use Prisma.ModelWhereInput types
const where: Prisma.OrderWhereInput = { makerAddress }
if (status) where.status = status as OrderStatus
// ❌ Wrong — loses type safety
const where: any = { makerAddress }
// ✅ Correct — batch queries to avoid N+1
const counts = await prisma.referral.groupBy({
by: ['referrerAddress'],
where: { referrerAddress: { in: addresses } },
_count: { id: true },
})
// ❌ Wrong — N+1 loop
for (const ref of referees) {
const count = await prisma.referral.count({ where: { referrerAddress: ref } })
}All route handlers must validate input with Zod before using it:
const schema = z.object({
address: z.string().min(1),
amount: z.coerce.number().positive(),
})
router.post('/', async (req, res, next) => {
const parsed = schema.safeParse(req.body)
if (!parsed.success) {
return res.status(400).json({ error: 'Validation failed', details: parsed.error.issues })
}
// use parsed.data safely from here
})import { log } from '../utils/logger'
// ✅ Structured logging
log.info({ orderId, pairSymbol }, '[Order] Created successfully')
log.error({ err, orderId }, '[Order] Failed to create')
// Development-only console logs
if (process.env.NODE_ENV !== 'production') {
console.log('Debug info:', data)
}Admin endpoints must always use requireAdmin middleware and must not expose the ADMIN_SECRET in responses:
router.post('/admin/action', requireAdmin, async (req, res, next) => {
// ...
})| Operation | Auth required | Pattern |
|---|---|---|
| Read public data | None | — |
| Wallet-signed mutations | sr25519 sig + nonce | verifyWalletActionSignature() |
| Admin operations | Bearer token | requireAdmin middleware |
| AI agent trades | API key | agentAuth(['TRADE_SPOT']) |
- Use nouns, not verbs in routes:
/ordersnot/createOrder - Use
next(err)in every catch block — no inline 500 responses - Always include radix in
parseInt:parseInt(str, 10) - Cap pagination limits:
Math.min(parseInt(str, 10) || 50, 200) - Return 201 for POST that creates a resource
- Return 204 for DELETE with no body (or 200 with the deleted object)
- Include
{ error, code, details? }in all error responses
# Feature branch
git checkout -b feature/TICKET-description
# Commit convention (Conventional Commits)
git commit -m "feat(affiliate): add multi-level referral tree batching"
git commit -m "fix(orders): remove redundant findUnique after update"
git commit -m "docs(api): add affiliate commission endpoint examples"
git commit -m "refactor(listing): migrate to next(err) pattern"
# Types: feat | fix | docs | refactor | test | chore | perf | security- New service functions must have unit tests
- New API endpoints must have at minimum a happy-path + invalid-input test
- No
anytype in test files - Use
beforeAll/afterAllfor DB cleanup (never leave test data)
describe('My Feature', () => {
beforeAll(async () => {
await prisma.myModel.deleteMany({ where: { isTestData: true } })
})
afterAll(async () => {
await prisma.myModel.deleteMany({ where: { isTestData: true } })
await prisma.$disconnect()
})
it('should do the thing', async () => {
// Arrange
const input = { ... }
// Act
const result = await myService.doThing(input)
// Assert
expect(result).toHaveProperty('id')
})
})- Edit
prisma/schema.prisma - Run
npx prisma migrate dev --name <description> - Update affected service types (don't use
prisma as any) - Update affected formatters/response shapes
- Add or update tests for affected endpoints
-
yarn typecheck→ 0 errors -
yarn lint→ 0 warnings on changed files -
yarn test→ all tests pass - Admin endpoints have
requireAdmin - All catch blocks use
next(err)not inline responses - New env vars added to
.env.example -
docs/API.mdupdated if routes changed