中文 | English
Thank you for considering contributing to this project!
- Node.js 20+
- Bun 1.3+
# Clone the repository
git clone https://github.com/du2333/flare-stack-blog.git
cd flare-stack-blog
# Install dependencies
bun install
# Configure environment variables
cp .env.example .env # Client-side variables
cp .dev.vars.example .dev.vars # Server-side variables
# Edit .env and .dev.vars to fill in the necessary configurations
# Configure Wrangler
cp wrangler.example.jsonc wrangler.jsonc
# Edit wrangler.jsonc with your resource IDs
# Start development server
bun devVisit http://localhost:3000 to view the application.
Before making changes to the business logic, it is recommended to read the Error Handling and Result Pattern Quickstart first.
Before every commit, ensure you pass the following checks:
bun check # Type checking + Linting + Formatting
bun run test # Run testsUse clear and descriptive commit messages following the Conventional Commits standard:
feat: add RSS feed feature
fix: resolve issue with lost login state
docs: update API documentation
refactor: rewrite caching layer
Each feature module follows a three-tier architecture:
features/<name>/
├── data/ # Data Layer: Pure Drizzle queries, no business logic
├── <name>.service.ts # Service Layer: Business logic + Cache orchestration
├── <name>.schema.ts # Zod schemas + Cache key factories
└── api/ # API Layer: Server Functions entry points
Data Layer Example:
// posts.data.ts
export const PostRepo = {
findPostById: (db: DB, id: number) =>
db.select().from(posts).where(eq(posts.id, id)).get(),
};Service Layer Example:
// posts.service.ts
export async function findPostBySlug(
context: DbContext & { executionCtx: ExecutionContext },
data: { slug: string },
) {
const fetcher = () => PostRepo.findPostBySlug(context.db, data.slug);
const version = await CacheService.getVersion(context, "posts:detail");
return CacheService.get(
context,
POSTS_CACHE_KEYS.detail(version, data.slug),
PostSchema,
fetcher,
);
}Follow these conventions:
Resultis ONLY used for expected business errors (e.g.,POST_NOT_FOUND,MEDIA_IN_USE).- Request-level errors (Authentication, Permissions, Rate Limits, CAPTCHA) are
thrown directly by middleware. - Services with no business errors return
Tdirectly, do not wrap inok(...). - Rely on TypeScript to infer return types by default, explicitly annotate type locks only at public API boundaries.
Example:
import { ok, err } from "@/lib/errors";
// Service Layer (with business errors -> Result)
export async function createTag(context: DbContext, name: string) {
const exists = await TagRepo.nameExists(context.db, name);
if (exists) return err({ reason: "TAG_NAME_ALREADY_EXISTS" });
const tag = await TagRepo.insert(context.db, { name });
return ok(tag);
}
// Caller (query/mutation convention: handle business errors in onSuccess)
const createTagMutation = useMutation({
mutationFn: (name: string) => createTagFn({ data: { name } }),
onSuccess: (result) => {
if (result.error) {
switch (result.error.reason) {
case "TAG_NAME_ALREADY_EXISTS":
toast.error("Tag already exists");
return;
default:
result.error.reason satisfies never; // Exhaustive check
return;
}
}
toast.success("Tag created");
},
});
// Service Layer (no business errors -> return T directly)
export async function getTags(context: DbContext) {
return TagRepo.findAll(context.db);
}TanStack Start middlewares inject dependencies sequentially:
dbMiddleware → sessionMiddleware → authMiddleware → adminMiddleware
Usage Examples:
// Public endpoint + Rate limiting
export const createCommentFn = createServerFn()
.middleware([
createRateLimitMiddleware({
capacity: 10,
interval: "1m",
key: "comments:create",
}),
])
.handler(({ data, context }) => CommentService.createComment(context, data));
// Public endpoint (Database only needed)
export const getPostsFn = createServerFn()
.middleware([dbMiddleware])
.handler(({ context }) => PostService.getPosts(context));
// Admin endpoint (Requires authentication + admin privileges)
export const updatePostFn = createServerFn()
.middleware([adminMiddleware]) // Automatically includes db + session + auth checks
.handler(({ data, context }) => PostService.updatePost(context, data));Dual-layer caching architecture:
| Layer | Technology | Purpose |
|---|---|---|
| CDN | Cache-Control headers | Edge caching, set via page headers or Hono routes |
| KV | Versioned Keys | Server-side caching, managed via CacheService |
Invalidation Patterns:
// Batch Invalidation: Bump version number
await CacheService.bumpVersion(context, "posts:list");
// Single Item Invalidation: Delete specific key
const version = await CacheService.getVersion(context, "posts:detail");
await CacheService.deleteKey(context, POSTS_CACHE_KEYS.detail(version, slug));Error handling conventions are uniformly maintained in the Error Handling and Result Pattern Quickstart, and won't be repeated here.
Query Key Factories:
export const POSTS_KEYS = {
all: ["posts"] as const,
lists: ["posts", "list"] as const, // Parent key (static, used for batch invalidation)
list: (
filters?: { tag?: string }, // Child key (function, used for specific queries)
) => ["posts", "list", filters] as const,
};Use ensureQueryData or prefetchQuery in route loaders to preload data:
// routes/_public/post/$slug.tsx
export const Route = createFileRoute("/_public/post/$slug")({
loader: async ({ context, params }) => {
// ensureQueryData: Fetch and cache, do not refetch if data already exists
const post = await context.queryClient.ensureQueryData(
postBySlugQuery(params.slug),
);
if (!post) throw notFound();
// prefetchQuery: Background preload (non-blocking for render)
void context.queryClient.prefetchQuery(relatedPostsQuery(params.slug));
return post;
},
component: PostPage,
});useSuspenseQuery: Used in conjunction with loaders, data is preloaded, rendering is synchronous.useQuery: Pure client-side fetching without preloading.
// SSR Scenario (preloaded by loader)
function PostPage() {
const { slug } = Route.useParams();
const { data: post } = useSuspenseQuery(postBySlugQuery(slug)); // Synchronous fetch
return <article>{post.content}</article>;
}
// Pure Client-side Scenario
function RelatedPosts({ slug }: { slug: string }) {
const { data } = useQuery(relatedPostsQuery(slug)); // Might show a loading state
// ...
}// Batch Invalidation
queryClient.invalidateQueries({ queryKey: POSTS_KEYS.lists });
// Exact Invalidation
queryClient.invalidateQueries({ queryKey: POSTS_KEYS.list({ tag: "React" }) });Use structured JSON logging to facilitate searching and filtering in Workers Observability:
// ✅ Good
console.log(JSON.stringify({ message: "cache hit", key: serializedKey }));
console.error(
JSON.stringify({
message: "image transform failed",
key,
error: String(error),
}),
);
// 🔴 Bad
console.log(`[Cache] HIT: ${serializedKey}`);
console.error("Image transform failed:", error);Critical business logs (request entry points, errors, important events) should use structured formats. Development debug logs can remain as they are.
| Type | Convention | Example |
|---|---|---|
| Component Files | kebab-case | post-item.tsx |
| Service Files | <name>.service.ts |
posts.service.ts |
| Data Files | <name>.data.ts |
posts.data.ts |
| Server Functions | camelCase + Fn |
getPostsFn |
| React Components | PascalCase | PostItem |
| Variables/Functions | camelCase | getPosts |
| Types/Interfaces | PascalCase | PostItemProps |
| Constants | SCREAMING_SNAKE_CASE | CACHE_CONTROL |
# Run all tests
bun run test
# Run specific module tests
bun run test posts
# Run logic for a single file
bun run test src/features/posts/posts.service.test.tsimport {
createAdminTestContext,
seedUser,
waitForBackgroundTasks,
testRequest,
} from "tests/test-utils";
// Create context
const context = createAdminTestContext();
await seedUser(context.db, context.session.user);
// Wait for background tasks
await waitForBackgroundTasks(context.executionCtx);
// Test Hono routes
const response = await testRequest(app, "/api/posts");Before submitting a PR, ensure you have:
- Passed
bun check(Type checks + Linting + Formatting) - Passed
bun run test - Added test coverage for new features
- Followed existing code patterns and naming conventions
If you have any questions, you can:
- Ask questions in GitHub Discussions
- Ask questions in the Telegram Group
- Refer to the development guides under the
.agent/skills/directory
Thank you for your contributions!