api

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Development Guide

API开发指南

You are working on the 100cims API (
packages/api
), a Next.js + Elysia hybrid backend.
你正在开发100cims API
packages/api
),这是一个基于Next.js + Elysia的混合后端项目。

Key Files

核心文件

FilePurpose
src/api/routes/index.ts
Elysia app composition, error handling
src/app/api/[[...slugs]]/route.ts
Next.js catch-all for Elysia
src/db/schema.ts
Drizzle schema (source of truth)
src/db/index.ts
Database client
src/api/routes/@shared/jwt.ts
JWT middleware
src/api/routes/@shared/s3.ts
S3 upload utilities
src/api/lib/sheets.ts
Google Sheets logging
drizzle.config.ts
Database connection config
文件用途
src/api/routes/index.ts
Elysia应用组合、错误处理
src/app/api/[[...slugs]]/route.ts
Next.js对Elysia的全捕获路由
src/db/schema.ts
Drizzle schema(数据源基准)
src/db/index.ts
数据库客户端
src/api/routes/@shared/jwt.ts
JWT中间件
src/api/routes/@shared/s3.ts
S3上传工具
src/api/lib/sheets.ts
Google Sheets日志记录工具
drizzle.config.ts
数据库连接配置

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

目录结构

  • /src/api/
    : All Elysia API code
    • /routes/
      : Route handlers (public, protected, @shared)
    • /schemas/
      : TypeBox validation schemas
    • /lib/
      : Utilities (sheets, dates, images, slug)
  • /src/db/
    : Database schema and client
  • /src/app/
    : Next.js pages and API catch-all route
  • /src/api/
    :所有Elysia API代码
    • /routes/
      :路由处理器(公开、受保护、@shared共享模块)
    • /schemas/
      :TypeBox验证schema
    • /lib/
      :工具类(Sheets、日期、图片、slug生成)
  • /src/db/
    :数据库schema与客户端
  • /src/app/
    :Next.js页面与API全捕获路由

Shared Utilities

共享工具类

FilePurpose
src/api/lib/slug.ts
generateSlug()
- URL-friendly slug generation
src/api/lib/images.ts
isBase64SizeValid()
- Image size validation
src/api/lib/sheets.ts
Google Sheets logging utilities
src/api/lib/dates.ts
Date formatting utilities
文件用途
src/api/lib/slug.ts
generateSlug()
- 生成URL友好的slug
src/api/lib/images.ts
isBase64SizeValid()
- 图片大小验证
src/api/lib/sheets.ts
Google Sheets日志记录工具
src/api/lib/dates.ts
日期格式化工具

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 routes
Folder-based routes: Group related endpoints in folders with an
index.ts
that composes them with a prefix. Each endpoint gets its own file.
/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.ts
组合并添加前缀。每个端点拥有独立文件。

Creating 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

添加新端点

  1. Create schema in
    /api/schemas/
  2. Create route file in
    /routes/public/
    or
    /protected/
  3. Import and use in
    /routes/index.ts
  4. Mobile app: Run
    yarn generate-api-types
  1. /api/schemas/
    中创建schema
  2. /routes/public/
    /protected/
    中创建路由文件
  3. /routes/index.ts
    中导入并使用
  4. 移动端应用:运行
    yarn generate-api-types

Database Migration

数据库迁移

  1. Update
    /src/db/schema.ts
  2. Run
    yarn drizzle-kit push
    (pushes to DB)
  3. Verify schema changes in database
  1. 更新
    /src/db/schema.ts
  2. 运行
    yarn drizzle-kit push
    (推送至数据库)
  3. 在数据库中验证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

环境变量

  • DATABASE_URL
    : PostgreSQL connection string
  • AUTH_SECRET
    : JWT signing secret
  • AWS_*
    : S3 credentials (region, bucket, access keys)
  • SHEETS_*
    : Google service account credentials
  • APP_NAME
    : Application name (used in S3 paths)
See
.env.example
for complete list.
  • DATABASE_URL
    :PostgreSQL连接字符串
  • AUTH_SECRET
    :JWT签名密钥
  • AWS_*
    :S3凭证(区域、存储桶、访问密钥)
  • SHEETS_*
    :Google服务账号凭证
  • APP_NAME
    :应用名称(用于S3路径)
完整列表请查看
.env.example

Swagger Documentation

Swagger文档

Available at
/api/swagger
during development. Auto-generated from:
  • Route tags
  • TypeBox schemas
  • OpenAPI metadata in route definitions
开发环境中可通过
/api/swagger
访问,自动生成自:
  • 路由标签
  • TypeBox schemas
  • 路由定义中的OpenAPI元数据

Database Schema

数据库Schema

See
/src/db/schema.ts
for full schema. Key tables:
  • user
    : OAuth accounts
  • mountain
    : Peak data (name, lat/lng, elevation, difficulty)
  • summit
    : User summit logs
  • plan
    : Group hiking plans
  • plan_attendee
    : Plan participants
  • plan_chat
    : Chat messages
  • challenge
    : Curated challenges
  • hiscores
    : Leaderboard
完整Schema请查看
/src/db/schema.ts
。核心表:
  • user
    :OAuth账户
  • 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控制台设置环境变量
  • 主分支提交后自动部署