api
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI Development Guide
API开发指南
You are working on the 100cims API (), a Next.js + Elysia hybrid backend.
packages/api你正在开发100cims API(),这是一个基于Next.js + Elysia的混合后端项目。
packages/apiKey Files
核心文件
| File | Purpose |
|---|---|
| Elysia app composition, error handling |
| Next.js catch-all for Elysia |
| Drizzle schema (source of truth) |
| Database client |
| JWT middleware |
| S3 upload utilities |
| Google Sheets logging |
| Database connection config |
| 文件 | 用途 |
|---|---|
| Elysia应用组合、错误处理 |
| Next.js对Elysia的全捕获路由 |
| Drizzle schema(数据源基准) |
| 数据库客户端 |
| JWT中间件 |
| S3上传工具 |
| Google Sheets日志记录工具 |
| 数据库连接配置 |
Architecture
架构设计
Hybrid Stack
混合技术栈
- Next.js 15 (App Router) for web pages and runtime
- Elysia 1.4 for API routes (mounted at )
/api/* - Drizzle ORM with PostgreSQL
- TypeBox for schema validation
- Next.js 15(App Router):用于网页页面与运行时环境
- Elysia 1.4:用于API路由(挂载在路径下)
/api/* - Drizzle ORM:搭配PostgreSQL使用
- TypeBox:用于schema验证
Why This Hybrid?
选择混合架构的原因
Elysia provides excellent TypeScript inference, OpenAPI generation, and performance while Next.js handles the server runtime and potential web pages.
Elysia提供出色的TypeScript类型推断、OpenAPI生成能力与性能表现,而Next.js则负责处理服务器运行时与潜在的网页页面需求。
Directory Structure
目录结构
- : All Elysia API code
/src/api/- : Route handlers (public, protected, @shared)
/routes/ - : TypeBox validation schemas
/schemas/ - : Utilities (sheets, dates, images, slug)
/lib/
- : Database schema and client
/src/db/ - : Next.js pages and API catch-all route
/src/app/
- :所有Elysia API代码
/src/api/- :路由处理器(公开、受保护、@shared共享模块)
/routes/ - :TypeBox验证schema
/schemas/ - :工具类(Sheets、日期、图片、slug生成)
/lib/
- :数据库schema与客户端
/src/db/ - :Next.js页面与API全捕获路由
/src/app/
Shared Utilities
共享工具类
| File | Purpose |
|---|---|
| |
| |
| Google Sheets logging utilities |
| Date formatting utilities |
| 文件 | 用途 |
|---|---|
| |
| |
| Google Sheets日志记录工具 |
| 日期格式化工具 |
Key Patterns
核心模式
Route Organization
路由组织
/api/routes/
├── @shared/ # Middleware, JWT, S3, types
├── public/ # No auth required
│ ├── mountains.route.ts
│ ├── challenge.route.ts
│ └── hiscores.route.ts
├── protected/ # JWT required
│ ├── summit.route.ts
│ ├── user.route.ts
│ ├── plan.route.ts
│ ├── mountains/ # Folder-based organization
│ │ ├── index.ts
│ │ ├── my-list.route.ts
│ │ └── update.route.ts
│ └── community-challenge/
│ ├── index.ts
│ ├── create.route.ts
│ ├── update.route.ts
│ └── delete.route.ts
└── index.ts # Compose all routesFolder-based routes: Group related endpoints in folders with an that composes them with a prefix. Each endpoint gets its own file.
index.ts/api/routes/
├── @shared/ # 中间件、JWT、S3、类型定义
├── public/ # 无需认证
│ ├── mountains.route.ts
│ ├── challenge.route.ts
│ └── hiscores.route.ts
├── protected/ # 需要JWT认证
│ ├── summit.route.ts
│ ├── user.route.ts
│ ├── plan.route.ts
│ ├── mountains/ # 基于文件夹的路由分组
│ │ ├── index.ts
│ │ ├── my-list.route.ts
│ │ └── update.route.ts
│ └── community-challenge/
│ ├── index.ts
│ ├── create.route.ts
│ ├── update.route.ts
│ └── delete.route.ts
└── index.ts # 组合所有路由基于文件夹的路由:将相关端点分组到文件夹中,通过组合并添加前缀。每个端点拥有独立文件。
index.tsCreating Routes
创建路由
typescript
import { Elysia } from 'elysia';
import { db } from '@/db';
import { userSchema } from '@/api/schemas';
export const userRoute = new Elysia({ prefix: '/user', tags: ['users'] })
.get('/:id', async ({ params }) => {
const user = await db.query.user.findFirst({
where: (u, { eq }) => eq(u.id, params.id)
});
return user;
}, {
detail: { summary: 'Get user by ID' },
params: userSchema.params,
response: userSchema.response
});typescript
import { Elysia } from 'elysia';
import { db } from '@/db';
import { userSchema } from '@/api/schemas';
export const userRoute = new Elysia({ prefix: '/user', tags: ['users'] })
.get('/:id', async ({ params }) => {
const user = await db.query.user.findFirst({
where: (u, { eq }) => eq(u.id, params.id)
});
return user;
}, {
detail: { summary: 'Get user by ID' },
params: userSchema.params,
response: userSchema.response
});Protected Routes
受保护路由
typescript
import { jwt } from '@/api/routes/@shared/jwt';
import { store } from '@/api/routes/@shared/store';
export const summitRoute = new Elysia({ prefix: '/summit', tags: ['summits'] })
.use(jwt)
.use(store)
.derive(async ({ bearer, store }) => {
const payload = await bearer(bearer);
store.userId = payload.userId;
})
.post('/', async ({ body, store }) => {
// store.userId available from JWT
const summit = await db.insert(summitTable).values({
userId: store.userId,
mountainId: body.mountainId
});
return summit;
});typescript
import { jwt } from '@/api/routes/@shared/jwt';
import { store } from '@/api/routes/@shared/store';
export const summitRoute = new Elysia({ prefix: '/summit', tags: ['summits'] })
.use(jwt)
.use(store)
.derive(async ({ bearer, store }) => {
const payload = await bearer(bearer);
store.userId = payload.userId;
})
.post('/', async ({ body, store }) => {
// store.userId 可从JWT中获取
const summit = await db.insert(summitTable).values({
userId: store.userId,
mountainId: body.mountainId
});
return summit;
});Database Queries
数据库查询
typescript
import { db } from '@/db';
import { user, summit, mountain } from '@/db/schema';
import { eq, desc } from 'drizzle-orm';
// Simple query
const users = await db.select().from(user).where(eq(user.id, userId));
// Join query
const summits = await db
.select({
id: summit.id,
mountainName: mountain.name,
date: summit.createdAt
})
.from(summit)
.leftJoin(mountain, eq(summit.mountainId, mountain.id))
.where(eq(summit.userId, userId))
.orderBy(desc(summit.createdAt));typescript
import { db } from '@/db';
import { user, summit, mountain } from '@/db/schema';
import { eq, desc } from 'drizzle-orm';
// 简单查询
const users = await db.select().from(user).where(eq(user.id, userId));
// 关联查询
const summits = await db
.select({
id: summit.id,
mountainName: mountain.name,
date: summit.createdAt
})
.from(summit)
.leftJoin(mountain, eq(summit.mountainId, mountain.id))
.where(eq(summit.userId, userId))
.orderBy(desc(summit.createdAt));Schema Validation
Schema验证
typescript
import { t } from 'elysia';
export const summitSchema = {
body: t.Object({
mountainId: t.String(),
date: t.Optional(t.String()),
image: t.Optional(t.String())
}),
response: {
200: t.Object({
id: t.String(),
mountainId: t.String(),
userId: t.String()
})
}
};typescript
import { t } from 'elysia';
export const summitSchema = {
body: t.Object({
mountainId: t.String(),
date: t.Optional(t.String()),
image: t.Optional(t.String())
}),
response: {
200: t.Object({
id: t.String(),
mountainId: t.String(),
userId: t.String()
})
}
};Pagination Pattern
分页模式
For paginated endpoints, use this pattern for backwards compatibility:
typescript
// Schema
export const PaginatedItemsSchema = t.Object({
items: t.Array(ItemSchema),
pagination: t.Object({
page: t.Number(),
pageSize: t.Number(),
totalItems: t.Number(),
totalPages: t.Number(),
hasMore: t.Boolean(),
}),
});
// Route handler - backwards compatible
const isPaginated = query.page !== undefined || query.limit !== undefined;
if (isPaginated) {
// Return paginated results with count query
return { items: results, pagination: { page, pageSize, totalItems, totalPages, hasMore } };
}
// No pagination params = return ALL results (backwards compatible)
return { items: results, pagination: { page: 1, pageSize: results.length, totalItems: results.length, totalPages: 1, hasMore: false } };Key: Old clients without pagination params get all results. New clients can paginate.
对于分页端点,使用以下模式以保证向后兼容性:
typescript
// Schema
export const PaginatedItemsSchema = t.Object({
items: t.Array(ItemSchema),
pagination: t.Object({
page: t.Number(),
pageSize: t.Number(),
totalItems: t.Number(),
totalPages: t.Number(),
hasMore: t.Boolean(),
}),
});
// 路由处理器 - 向后兼容
const isPaginated = query.page !== undefined || query.limit !== undefined;
if (isPaginated) {
// 返回带计数查询的分页结果
return { items: results, pagination: { page, pageSize, totalItems, totalPages, hasMore } };
}
// 无分页参数 = 返回所有结果(向后兼容)
return { items: results, pagination: { page: 1, pageSize: results.length, totalItems: results.length, totalPages: 1, hasMore: false } };核心要点:未传入分页参数的旧客户端将获取所有结果,新客户端则可使用分页功能。
Common Tasks
常见任务
Add New Endpoint
添加新端点
- Create schema in
/api/schemas/ - Create route file in or
/routes/public//protected/ - Import and use in
/routes/index.ts - Mobile app: Run
yarn generate-api-types
- 在中创建schema
/api/schemas/ - 在或
/routes/public/中创建路由文件/protected/ - 在中导入并使用
/routes/index.ts - 移动端应用:运行
yarn generate-api-types
Database Migration
数据库迁移
- Update
/src/db/schema.ts - Run (pushes to DB)
yarn drizzle-kit push - Verify schema changes in database
- 更新
/src/db/schema.ts - 运行(推送至数据库)
yarn drizzle-kit push - 在数据库中验证schema变更
Image Upload to S3
图片上传至S3
typescript
import { putImageOnS3 } from '@/api/routes/@shared/s3';
const key = `${process.env.APP_NAME}/user/avatar/${userId}.jpeg`;
await putImageOnS3(key, buffer);typescript
import { putImageOnS3 } from '@/api/routes/@shared/s3';
const key = `${process.env.APP_NAME}/user/avatar/${userId}.jpeg`;
await putImageOnS3(key, buffer);Log to Google Sheets
记录日志至Google Sheets
typescript
import { addRowToSheets, ERRORS_SPREADSHEET } from '@/api/lib/sheets';
await addRowToSheets(ERRORS_SPREADSHEET, [
'error_type',
'status_code',
'url',
'message'
]);typescript
import { addRowToSheets, ERRORS_SPREADSHEET } from '@/api/lib/sheets';
await addRowToSheets(ERRORS_SPREADSHEET, [
'error_type',
'status_code',
'url',
'message'
]);Environment Variables
环境变量
- : PostgreSQL connection string
DATABASE_URL - : JWT signing secret
AUTH_SECRET - : S3 credentials (region, bucket, access keys)
AWS_* - : Google service account credentials
SHEETS_* - : Application name (used in S3 paths)
APP_NAME
See for complete list.
.env.example- :PostgreSQL连接字符串
DATABASE_URL - :JWT签名密钥
AUTH_SECRET - :S3凭证(区域、存储桶、访问密钥)
AWS_* - :Google服务账号凭证
SHEETS_* - :应用名称(用于S3路径)
APP_NAME
完整列表请查看。
.env.exampleSwagger Documentation
Swagger文档
Available at during development. Auto-generated from:
/api/swagger- Route tags
- TypeBox schemas
- OpenAPI metadata in route definitions
开发环境中可通过访问,自动生成自:
/api/swagger- 路由标签
- TypeBox schemas
- 路由定义中的OpenAPI元数据
Database Schema
数据库Schema
See for full schema. Key tables:
/src/db/schema.ts- : OAuth accounts
user - : Peak data (name, lat/lng, elevation, difficulty)
mountain - : User summit logs
summit - : Group hiking plans
plan - : Plan participants
plan_attendee - : Chat messages
plan_chat - : Curated challenges
challenge - : Leaderboard
hiscores
完整Schema请查看。核心表:
/src/db/schema.ts- :OAuth账户
user - :山峰数据(名称、经纬度、海拔、难度)
mountain - :用户登顶记录
summit - :团体徒步计划
plan - :计划参与者
plan_attendee - :聊天消息
plan_chat - :精选挑战
challenge - :排行榜
hiscores
Error Handling
错误处理
Global error handler in :
/routes/index.ts- Logs all errors to Google Sheets
- Returns appropriate HTTP status codes
- Distinguishes ValidationError, ParseError, generic errors
全局错误处理器位于:
/routes/index.ts- 将所有错误记录至Google Sheets
- 返回合适的HTTP状态码
- 区分ValidationError、ParseError与通用错误
Deployment
部署
Vercel (configured in root ):
vercel.json- Builds from
packages/api - Environment variables set in Vercel dashboard
- Automatic deployments on main branch
通过Vercel部署(根目录中配置):
vercel.json- 从构建
packages/api - 在Vercel控制台设置环境变量
- 主分支提交后自动部署