Cursor tends to generate repetitive boilerplate when creating REST endpoints, duplicating error handling, validation, and response formatting across every route. By setting up .cursor/rules/ with DRY principles, creating shared middleware and utility files for Cursor to reference, and using Composer to generate entire features at once, you can get Cursor to produce clean, reusable code with shared abstractions.
Reducing boilerplate in Cursor-generated REST code
When you ask Cursor to generate multiple REST endpoints, each one often gets its own copy of error handling, validation, response wrapping, and logging code. This makes the codebase harder to maintain. This tutorial shows you how to create shared utilities that Cursor references, and rules that prevent it from duplicating common patterns across endpoints.
Prerequisites
- Cursor installed with a REST API project (Express, NestJS, Fastify, etc.)
- Basic understanding of middleware and utility patterns
- Familiarity with Cmd+I (Composer) and @file references
- An existing project with at least one REST endpoint
Step-by-step guide
Create a DRY code generation rule
Create a DRY code generation rule
Add a project rule that tells Cursor to import shared utilities instead of inlining common patterns. List the specific abstractions that exist in your project so Cursor knows what to reference.
1---2description: Enforce DRY patterns in REST endpoint generation3globs: "*.controller.ts,*.route.ts,*.router.ts"4alwaysApply: true5---67# DRY Code Rules for REST Endpoints8- NEVER duplicate error handling logic — use the asyncHandler wrapper9- NEVER duplicate response formatting — use ApiResponse.success/error10- NEVER duplicate validation — use validateRequest middleware11- NEVER inline pagination logic — use the paginate utility12- ALWAYS import shared utilities from @/lib/13- ALWAYS use the base controller/router pattern for CRUD endpoints14- Keep route handlers under 15 lines by delegating to services1516## Required Imports:17```typescript18import { asyncHandler } from '@/lib/async-handler';19import { ApiResponse } from '@/lib/api-response';20import { validateRequest } from '@/middleware/validate';21import { paginate } from '@/lib/pagination';22```Expected result: Cursor imports shared utilities instead of duplicating error handling, validation, and response formatting in each endpoint.
Create shared utility files for Cursor to reference
Create shared utility files for Cursor to reference
Build the utility files that your rules reference. Cursor needs real files to import from. Start with an async handler wrapper and a standardized API response format. Reference these files with @file in your prompts.
1import { Request, Response, NextFunction } from 'express';23export const asyncHandler = (4 fn: (req: Request, res: Response, next: NextFunction) => Promise<void>5) =>6 (req: Request, res: Response, next: NextFunction): void => {7 Promise.resolve(fn(req, res, next)).catch(next);8 };910export class ApiResponse<T> {11 constructor(12 public success: boolean,13 public data: T | null,14 public message: string,15 public meta?: Record<string, unknown>16 ) {}1718 static ok<T>(data: T, message = 'Success', meta?: Record<string, unknown>) {19 return new ApiResponse(true, data, message, meta);20 }2122 static created<T>(data: T, message = 'Created') {23 return new ApiResponse(true, data, message);24 }2526 static error(message: string, meta?: Record<string, unknown>) {27 return new ApiResponse(false, null, message, meta);28 }29}Expected result: Shared utilities exist as real files that Cursor can import in generated endpoints.
Generate a new endpoint referencing shared utilities
Generate a new endpoint referencing shared utilities
Open Composer with Cmd+I and ask Cursor to generate a new set of endpoints. Reference both the rule file and the shared utilities so Cursor uses them instead of creating inline duplicates. Specify that handlers should be short and delegate to services.
1@dry-rest.mdc @src/lib/api-response.ts @src/lib/async-handler.ts23Create a complete products API with these endpoints:4- GET /api/products (list with pagination)5- GET /api/products/:id (single product)6- POST /api/products (create with validation)7- PUT /api/products/:id (update with validation)8- DELETE /api/products/:id (delete)910Use asyncHandler for every route. Use ApiResponse for all responses.11Use validateRequest middleware for POST and PUT.12Keep each handler under 10 lines. Delegate logic to ProductService.Pro tip: Use Composer Agent mode (Cmd+I) for multi-file generation. It creates the route, service, and validation files in one session, maintaining consistent imports across all files.
Expected result: Cursor generates a router, service, and validation schema with all routes using asyncHandler and ApiResponse from shared utilities.
Refactor existing duplicated code with Cursor
Refactor existing duplicated code with Cursor
If you already have duplicated boilerplate across routes, use Cursor Chat to identify and extract shared patterns. Reference the folder containing your routes so Cursor can analyze all of them together.
1@dry-rest.mdc @src/routes/23Analyze all route files in this folder. Identify duplicated patterns:41. Error handling try/catch blocks that could use asyncHandler52. Response formatting that could use ApiResponse63. Validation logic that could be extracted to middleware74. Pagination code that could use a shared utility89For each duplication, show the refactored version using shared utilities.10Process one route file at a time.Expected result: Cursor identifies duplicated patterns across your routes and shows refactored versions using shared utilities for each file.
Create a base CRUD factory for maximum reuse
Create a base CRUD factory for maximum reuse
For projects with many similar resources, create a CRUD factory that generates standard endpoints from a configuration object. Cursor can then generate new resources by calling the factory instead of duplicating entire route files.
1import { Router } from 'express';2import { asyncHandler } from '@/lib/async-handler';3import { ApiResponse } from '@/lib/api-response';4import { paginate } from '@/lib/pagination';56interface CrudConfig<T> {7 service: {8 findAll: (page: number, limit: number) => Promise<{ data: T[]; total: number }>;9 findById: (id: string) => Promise<T | null>;10 create: (data: Partial<T>) => Promise<T>;11 update: (id: string, data: Partial<T>) => Promise<T>;12 remove: (id: string) => Promise<void>;13 };14 validateCreate?: any;15 validateUpdate?: any;16}1718export const createCrudRouter = <T>(config: CrudConfig<T>): Router => {19 const router = Router();20 const { service } = config;2122 router.get('/', asyncHandler(async (req, res) => {23 const { page, limit } = paginate(req.query);24 const result = await service.findAll(page, limit);25 res.json(ApiResponse.ok(result.data, 'Success', { total: result.total, page, limit }));26 }));2728 router.get('/:id', asyncHandler(async (req, res) => {29 const item = await service.findById(req.params.id);30 if (!item) return res.status(404).json(ApiResponse.error('Not found'));31 res.json(ApiResponse.ok(item));32 }));3334 router.post('/', asyncHandler(async (req, res) => {35 const item = await service.create(req.body);36 res.status(201).json(ApiResponse.created(item));37 }));3839 return router;40};Expected result: A reusable CRUD factory that Cursor can reference to generate new resources with zero boilerplate duplication.
Complete working example
1---2description: Enforce DRY patterns in REST endpoint generation3globs: "*.controller.ts,*.route.ts,*.router.ts"4alwaysApply: true5---67# DRY Code Rules for REST Endpoints8- NEVER duplicate error handling logic — use asyncHandler from @/lib/async-handler9- NEVER duplicate response formatting — use ApiResponse from @/lib/api-response10- NEVER duplicate validation — use validateRequest middleware from @/middleware/validate11- NEVER inline pagination logic — use paginate from @/lib/pagination12- ALWAYS import shared utilities from @/lib/13- ALWAYS use the CRUD factory for standard REST resources14- Keep route handlers under 15 lines by delegating to services15- Extract any pattern used in 3+ routes into a shared utility1617## Required Imports:18```typescript19import { asyncHandler } from '@/lib/async-handler';20import { ApiResponse } from '@/lib/api-response';21import { validateRequest } from '@/middleware/validate';22import { paginate } from '@/lib/pagination';23```2425## Route Handler Pattern:26```typescript27router.get('/', asyncHandler(async (req, res) => {28 const { page, limit } = paginate(req.query);29 const result = await service.findAll(page, limit);30 res.json(ApiResponse.ok(result.data, 'Success', { total: result.total }));31}));32```3334## Anti-Pattern (NEVER):35```typescript36router.get('/', async (req, res) => {37 try {38 const page = parseInt(req.query.page) || 1; // duplicated pagination39 const result = await service.findAll(page);40 res.json({ success: true, data: result }); // duplicated response format41 } catch (err) {42 res.status(500).json({ error: err.message }); // duplicated error handling43 }44});45```Common mistakes when reducing Boilerplate in Cursor-Generated Code
Why it's a problem: Asking Cursor to generate endpoints one at a time in separate sessions
How to avoid: Use Composer Agent mode to generate all related endpoints in a single session so Cursor can reference shared code it just created.
Why it's a problem: Having rules without actual utility files to import
How to avoid: Create the utility files first, then reference them with @file in your prompts alongside the rules.
Why it's a problem: Writing shared utilities that are too abstract
How to avoid: Keep shared utilities simple and include clear usage examples in your rules file. Cursor follows concrete patterns better than abstract frameworks.
Best practices
- Create shared utility files before asking Cursor to generate endpoints
- Include usage examples in your rules file so Cursor sees the import pattern
- Use Composer Agent mode for multi-file generation to maintain consistency
- Extract any pattern that appears in three or more routes into a shared utility
- Keep route handlers under 15 lines by delegating business logic to services
- Use factory patterns for standard CRUD resources to eliminate boilerplate entirely
- Reference both the rules file and utility files with @ symbols in every prompt
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have duplicated error handling, response formatting, and validation across 12 Express.js route files. Help me extract shared asyncHandler, ApiResponse, and validateRequest utilities, and show how to refactor one route file to use them.
@dry-rest.mdc @src/lib/api-response.ts @src/lib/async-handler.ts Generate a complete orders API with CRUD endpoints. Every route must use asyncHandler and ApiResponse from the shared libraries. No duplicated error handling or response formatting. Keep handlers under 10 lines.
Frequently asked questions
Why does Cursor keep duplicating try/catch blocks?
Cursor defaults to explicit error handling in every function because it is the safest pattern. Create an asyncHandler wrapper and reference it in your rules. Cursor will import it once it sees the utility file.
Should I use a CRUD factory or generate individual routes?
Use a factory for resources with standard CRUD patterns. Generate individual routes for endpoints with custom business logic that does not fit the standard pattern.
How do I handle endpoints that need slightly different behavior?
Use the factory for the standard operations and add custom routes alongside it. Your rules should allow both patterns: factory for standard CRUD, individual handlers for custom logic.
Will these patterns work with NestJS controllers?
Yes, but NestJS has its own DRY patterns like interceptors, pipes, and guards. Adapt the rules to reference NestJS-specific abstractions like @UseInterceptors and @UseGuards instead of Express middleware.
How do I prevent Cursor from generating too much abstraction?
Specify in your rules that abstractions should only be created when a pattern appears in three or more routes. This prevents over-engineering while still eliminating real duplication.
Can RapidDev help design our API architecture?
Yes. RapidDev designs REST API architectures with proper shared utilities, middleware patterns, and Cursor rules that keep generated code DRY and maintainable as the project scales.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation