Instruction file imported from AsharibAli/ramadan-prompting-nights (
.cursor/rules/api.mdc). Copyright stays with the author.
Backend API Architecture and Development Guidelines
You are an expert TypeScript backend engineer specializing in building modern, type-safe APIs. Your expertise covers Hono for HTTP routing, Drizzle ORM for database operations, and React Query for frontend integration.
<core_architecture>
<tech_stack>
- Server & Routing: Hono
- Database ORM: Drizzle with PostgreSQL
- Frontend Integration: React Query
- Authentication: Clerk
- Validation: Zod with zValidator </tech_stack>
<project_structure> apps/ ├── api/ │ ├── src/ │ │ ├── modules/ # Feature-based modules │ │ │ ├── [module]/ # e.g., posts, webhooks │ │ │ │ ├── [module].routes.ts # Route definitions │ │ │ │ └── [module].service.ts # Business logic & DB operations │ │ ├── pkg/ # Shared utilities and middleware │ │ └── index.ts # Main application setup └── web/ └── src/ └── api/ └── [module].api.ts # React Query hooks
packages/ └── db/ ├── src/ │ ├── schema.ts # Database schema definitions │ ├── types.ts # Shared TypeScript types │ ├── index.ts # Main exports │ └── util/ # Database utilities </project_structure>
<module_development_guidelines>
1. Database layer
- If the request needs a new column or addition database fields start by creating and updating schema.ts
- Leverage types.ts inside packages/db to create new zod schemas for the newly created tables or columns and eg:
export type Post = InferSelectModel<typeof schema.posts>;
export type NewPost = InferInsertModel<typeof schema.posts>;
export const postInsertSchema = createInsertSchema(schema.posts).omit({ userId: true });
export const postSelectSchema = createSelectSchema(schema.posts);
2. Service Layer ([module].service.ts)
- Implement business logic
- Handle database operations using Drizzle
- Return strongly typed responses
- Keep services focused and modular
- Import db from the
packages/db
Example service implementation:
`[module].service.ts`
import { db, eq, items} from "@repo/db"
export const moduleService = {
async getItems() {
return db.select().from(items);
},
async createItem(data: NewItem) {
return db.insert(items).values(data).returning();
}
};
3. Route Layer ([module].routes.ts)
- Define endpoints using Hono router
- Implement request validation using zValidator. You can leverage the created zod schemas from drizzle zod.
- Apply authentication middleware where needed
- Structure routes logically by resource
- Use custom error classes instead of manual status responses - Import from
@/pkg/errorsand throw appropriate errors - Avoid wrapping route handlers in try-catch blocks - let error middleware handle exceptions
- Delegate all business logic and error handling to the service layer
Example route implementation:
import { NotFoundError } from "@/pkg/errors";
const moduleRoutes = new Hono()
.use(auth(),requireAuth)
.get("/", async (c) => {
const items = await moduleService.getItems();
return c.json(items);
})
.post("/", zValidator("json", insertSchema), async (c) => {
const data = c.req.valid("json");
const userId = getUserId(c);
const result = await moduleService.createItem({ ...data, userId });
// If results could be undefined or otherwise, throw proper error
if (!result){
throw new NotFoundError("Item not found");
}
return c.json(result);
});
After the route is created, you must add it to the apps/api/src/index.ts route so its accessable by the frontend.
</module_development_guidelines>
<frontend_integration>
When fetching data from the backend api on the client, use the following guidelines
post.api.ts
import { apiRpc, getApiClient, InferRequestType, callRpc } from "./client";
const $createPost = apiRpc.posts.$post;
// Simple get
export async function getPosts() {
const client = await getApiClient();
return callRpc(client.posts.$get());
}
// Safely leverage the typed params elsewhere within the nextjs application
export type CreatePostParams = InferRequestType<typeof $createPost>["json"];
export async function createPost(params: CreatePostParams) {
const client = await getApiClient();
return callRpc(client.posts.$post({ json: params }));
}
</frontend_integration> <package_management>
- Use
pnpmas the primary package manager for the project - Install dependencies using
pnpm add [package-name] - Install dev dependencies using
pnpm add -D [package-name] - Install workspace dependencies using
pnpm add -w [package-name]</package_management>
<development_guidelines>
Running Scripts
- Use
bunas the runtime environment and script runner - Execute scripts defined in package.json using
bun run [script-name] - Run TypeScript files directly using
bun [file.ts]
Monorepo
The project use turbo repo. To run everything, use the turbo dev script from the root folder.
Type Safety
- Use Drizzle schemas for database types
- Share types between frontend and backend using a shared package
- Leverage zod for runtime validation
Error Handling
- Use custom error classes instead of manual JSON responses - Import and throw errors from
@/pkg/errors/error - Available error classes:
NotFoundError,BadRequestError,UnauthorizedError,ForbiddenError,ConflictError,UnprocessableEntityError,InternalServerError,TooManyRequestsError - NEVER return manual status responses like
return c.json({error: "message"}, 401)- always throw proper error classes - Implement consistent error handlers across all routes
- Handle edge cases by throwing appropriate error classes
- Use the logger from the package @repo/logger with object inside, i.e
logger.error({...})instead ofconsole.error
Error Class Usage Examples:
// ❌ DON'T DO THIS
if (!user) {
return c.json({ error: "User not found" }, 404);
}
// ✅ DO THIS INSTEAD
import { NotFoundError, BadRequestError, ConflictError } from "@/pkg/errors";
if (!user) {
throw new NotFoundError("User not found");
}
if (email && await userExists(email)) {
throw new ConflictError("User with this email already exists");
}
if (!isValidInput(data)) {
throw new BadRequestError("Invalid input data provided");
}
Authentication & Authorization
- Use Clerk middleware for authentication
- Implement role-based access control where needed
- Validate user permissions at the route level
- Keep authentication logic in middleware
API Design Principles
- Follow RESTful conventions
- Use consistent naming patterns
- Implement proper request validation
- Structure endpoints by resource/module
- Keep routes clean and delegate logic to services
Database Operations
- Use Drizzle for all database interactions
- Implement proper migrations
- Handle transactions when needed
- Write efficient queries
- Use appropriate indexes </development_guidelines>
<dev_workflow>
Creating a New Module for different buisness logic
- Create module directory in api/src/modules/[module]
- Define routes in [module].routes.ts
- Implement service logic in [module].service.ts
- Add route to main application in index.ts
- Create frontend integration in web/src/api/[module].api.ts </dev_workflow>
Testing Requirements
- If needed use vitest to create testing files inside of the modules, [module].test.ts
Code Quality
- For methods with more than one argument, use object destructuring:
function myMethod({ param1, param2 }: MyMethodParams) {...}. </best_practices>
<example_api_workflow> Here's a complete example of a typical module:
(if required) create new schemas. // packages/db/schema.ts .. new schema files
Then:
// posts.service.ts
import { db, posts, type NewPost } from "@repo/db";
export const postService = {
async getPosts() {
return db.select().from(posts);
},
async createPost(post: NewPost) {
return db.insert(posts).values(post).returning();
}
};
// posts.routes.ts
import { Hono } from "hono";
import { auth, requireAuth, getUserId } from "@/pkg/middleware/clerk-auth";
import { postService } from "./post.service";
import { zValidator } from "@/pkg/util/validator-wrapper";
import { postInsertSchema } from "@repo/db";
export const postRoutes = new Hono()
.use(auth(),requireAuth)
.get("/", async (c) => {
const posts = await postService.getPosts();
return c.json(posts);
})
.post("/", zValidator("json", postInsertSchema), async (c) => {
const data = c.req.valid("json");
const userId = getUserId(c);
const post = await postService.createPost({ ...data, userId });
return c.json(post);
});
- Use the RPC-style API client for type-safe API calls
- Leverage
InferRequestTypefor parameter typing - Export plain async functions for API operations
- Optionally create React Query hooks when needed
// posts.api.ts (Frontend)
import { apiRpc, getApiClient, InferRequestType, callRpc } from "./client";
const $createPost = apiRpc.posts.$post;
export async function getPosts() {
const client = await getApiClient();
return callRpc(client.posts.$get());
}
export async function createPost(params: InferRequestType<typeof $createPost>["json"]) {
const client = await getApiClient();
return callRpc(client.posts.$post({ json: params }));
return response.json();
}
</example_api_workflow>