Skip to content

Commit 3d3d783

Browse files
committed
endpoints and forms documentation updates
1 parent f2aec3e commit 3d3d783

2 files changed

Lines changed: 17 additions & 63 deletions

File tree

.claude/skills/general-practices/backend-endpoints/SKILL.md

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ description: Guide for creating backend API endpoints in FinishLine following th
1111

1212
FinishLine's backend is an Express.js application written in TypeScript. All request handling is split into three distinct layers with clear responsibilities:
1313

14-
1. **Routes** define the HTTP method and path, declare validation rules using `express-validator`, and point to a controller method. They contain zero business logic.
15-
2. **Controllers** are thin glue — they extract data from `req.params`, `req.body`, and `req.query`, call the appropriate service method, and return the result as JSON. They MUST delegate all errors to Express via `next(error)`.
14+
1. **Routes** define the HTTP method and path, declare validation rules using `express-validator`, and point to a controller method.
15+
2. **Controllers** are lightweight wrappers around the service functions. They extract data from `req.params`, `req.body`, and `req.query`, call the appropriate service method, and return the result as JSON. They also catch service errors and delegate to Express via `next(error)`.
1616
3. **Services** contain all business logic: permission checks, database queries via Prisma, data transformation, and side effects (Slack notifications, Google integrations, etc.). Services throw custom exceptions when something goes wrong.
1717

18-
Two key objects are available on every request thanks to global middleware: `req.currentUser` (the authenticated `User`) and `req.organization` (the Prisma `Organization` record). These are set by the `getUserAndOrganization` middleware in `src/backend/index.ts` and typed via `src/backend/custom.d.ts`.
18+
Two key objects are available on every request from middleware: `req.currentUser` (the authenticated `User`) and `req.organization` (the Prisma `Organization` record). These are set by the `getUserAndOrganization` middleware in `src/backend/index.ts` and typed via `src/backend/custom.d.ts`.
1919

2020
## Architecture
2121

@@ -24,7 +24,7 @@ Two key objects are available on every request thanks to global middleware: `req
2424
2525
2626
┌──────────────┐ JWT validated, user
27-
│ Middleware │ and organization
27+
│ Middleware │ and organization
2828
│ (global) │ attached to req
2929
└──────┬───────┘
3030
@@ -56,19 +56,13 @@ Two key objects are available on every request thanks to global middleware: `req
5656

5757
If a service throws an exception, it bubbles up through the controller's `next(error)` call and is caught by the global `errorHandler` middleware registered at the bottom of `src/backend/index.ts`.
5858

59-
## File Locations
59+
## File Structure
6060

61-
| Layer | Path | Naming |
62-
| ------------- | ------------------------------------------------------ | --------------------------------------------- |
63-
| Entry point | `src/backend/index.ts` ||
64-
| Routes | `src/backend/src/routes/{feature}.routes.ts` | `{feature}Router` |
65-
| Controllers | `src/backend/src/controllers/{feature}.controllers.ts` | `{Feature}Controller` class |
66-
| Services | `src/backend/src/services/{feature}.services.ts` | `{Feature}Service` class |
67-
| Validation | `src/backend/src/utils/validation.utils.ts` | Shared validators |
68-
| Errors | `src/backend/src/utils/errors.utils.ts` | `HttpException` subclasses |
69-
| Express types | `src/backend/custom.d.ts` | `currentUser` and `organization` on `Request` |
61+
Finishline uses a layer-based architecture, where 3 folders (routes, controllers, services) contain all the files related to that layer for every domain. The naming convention looks like:
7062

71-
For query args and transformers, see the [query-args-and-transformers](../query-args-and-transformers/SKILL.md) skill.
63+
- `src/backend/src/routes/{feature}.routes.ts`
64+
- `src/backend/src/controllers/{feature}.controllers.ts`
65+
- `src/backend/src/services/{feature}.services.ts`
7266

7367
## How Endpoint URLs Work
7468

@@ -179,12 +173,10 @@ export default class CalendarController {
179173

180174
- Every method MUST be `static async` with signature `(req: Request, res: Response, next: NextFunction)`.
181175
- Every method body MUST be wrapped in `try { ... } catch (error: unknown) { next(error); }`.
182-
- NEVER handle errors directly in the controller. Always call `next(error)`.
183176
- Extract URL params with: `const { id } = req.params as Record<string, string>;`
184177
- **Parse date strings to `Date` objects in the controller** before passing to the service: `new Date(startTime)`.
185-
- Always pass `req.currentUser` and `req.organization` to service methods that need them.
178+
- Pass `req.currentUser` and `req.organization` to service methods that need them.
186179
- Return `res.status(200).json(result)` for all successful responses.
187-
- Controllers contain ZERO business logic — no permission checks, no database queries.
188180

189181
### Step 4: Write the Service Method
190182

@@ -272,7 +264,7 @@ export default class CalendarService {
272264
- ALWAYS filter `dateDeleted: null` on queries at both the top level and within nested includes/selects.
273265
- Deleting an entity MUST be a soft delete (`dateDeleted: new Date()`), never `prisma.*.delete()`.
274266

275-
For query args and transformer patterns, see the [query-args-and-transformers](../query-args-and-transformers/SKILL.md) skill.
267+
For query args and transformer patterns, see the [query-args-and-transformers](../query-args-and-transformers/SKILL.md) document.
276268

277269
### Step 5: Deciding the Access Level
278270

@@ -310,20 +302,7 @@ Match the exception class to the level: `AccessDeniedGuestException` for `notGue
310302

311303
## Error Handling
312304

313-
Services throw exceptions from `src/backend/src/utils/errors.utils.ts`. The global `errorHandler` middleware catches them.
314-
315-
| Exception | Status | When to Use |
316-
| ---------------------------------------- | ------ | ----------------------------------------- |
317-
| `HttpException(status, msg)` | any | General-purpose with custom status |
318-
| `NotFoundException(name, id)` | 404 | Entity not found |
319-
| `DeletedException(name, id)` | 404 | Entity is soft-deleted |
320-
| `AccessDeniedException(msg?)` | 403 | Generic permission failure |
321-
| `AccessDeniedAdminOnlyException(action)` | 403 | Non-admin attempting admin action |
322-
| `AccessDeniedMemberException(action)` | 403 | Guest/member attempting restricted action |
323-
| `AccessDeniedGuestException(action)` | 403 | Guest attempting non-guest action |
324-
| `InvalidOrganizationException(item)` | 400 | Entity not in current org |
325-
326-
The `name` parameter for `NotFoundException` and `DeletedException` MUST be one of the values in the `ExceptionObjectNames` type union in `errors.utils.ts`. Add your entity to that type if it's not listed.
305+
Services throw exceptions from `src/backend/src/utils/errors.utils.ts`. The global `errorHandler` middleware catches them. The `name` parameter for `NotFoundException` and `DeletedException` MUST be one of the values in the `ExceptionObjectNames` type union in `errors.utils.ts`. Add your entity to that type if it's not listed.
327306

328307
## Validation Helpers
329308

@@ -398,9 +377,3 @@ For complex reusable validators, spread them: `...descriptionBulletsValidators`.
398377
- [ ] Multiple writes wrapped in `prisma.$transaction()`
399378
- [ ] All imports use `.js` extensions
400379
- [ ] Router registered in `index.ts` (if new feature)
401-
402-
## Migration Notes
403-
404-
> New code MUST follow the patterns above. When modifying existing files, update them to match where practical.
405-
406-
`work-packages.routes.ts` contains one endpoint using the `DELETE` HTTP method (`workPackagesRouter.delete('/:wbsNum/delete', ...)`). This is legacy. The prescribed pattern is `POST` for all mutations including deletions. When touching this endpoint, migrate it to `POST`.

.claude/skills/general-practices/frontend-forms/SKILL.md

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ data changes.
6969
- **NERDraggableFormModal:** `src/frontend/src/components/NERDraggableFormModal.tsx`
7070
- **ReactHookTextField:** `src/frontend/src/components/ReactHookTextField.tsx`
7171
- **ReactHookEditableList:** `src/frontend/src/components/ReactHookEditableList.tsx`
72-
- **Modal forms:** `src/frontend/src/pages/{Feature}/{Components}/XFormModal.tsx`
73-
- **Full-page forms:** `src/frontend/src/pages/{Feature}Form/XFormView.tsx`
7472

7573
## Core Concepts
7674

@@ -89,8 +87,7 @@ management. It accepts these key props:
8987
NERFormModal internally:
9088

9189
1. Wraps `onFormSubmit` to call `reset()` after the submit callback
92-
2. Calls `reset()` when the modal is closed via `onHide`
93-
3. Renders a `<form>` element with `noValidate` and `e.stopPropagation()`
90+
2. Renders a `<form>` element with `noValidate` and `e.stopPropagation()`
9491

9592
### useForm Setup
9693

@@ -208,6 +205,8 @@ For `Autocomplete` (multi-select):
208205
/>
209206
```
210207

208+
NOTE: For taking in a user as an input to a form, always prefer autocomplete over select, and in general only show members as options.
209+
211210
## Step-by-Step: Creating a Modal Form
212211

213212
### Step 1: Define the Form Values Interface and Schema
@@ -372,6 +371,8 @@ const EditThingModal = ({ showModal, handleClose, thing }: EditThingModalProps)
372371
};
373372
```
374373

374+
NOTE: while `CreateXModal.tsx` and `EditXModal.tsx` will typically be dealing with the same payload structure to pass to their mutation hooks, this is not always the case. To allow for differences between create and edit functionality, you can create a type with optional fields that contain all the data needed for both create and update mutations, and use that as the input parameter type for your onSubmit. In the form, you can determine if you are are editting or creating based on the detault values. For an example of these concepts look at the calendar event form.
375+
375376
## Step-by-Step: Creating a Full-Page Form
376377

377378
Full-page forms (like Work Package create/edit) skip NERFormModal and
@@ -597,23 +598,3 @@ These files demonstrate the prescribed patterns well:
597598
- [ ] Form submit handler uses `e.stopPropagation()`
598599
- [ ] `<form>` element has `noValidate` attribute
599600
- [ ] Error messages display via `<FormHelperText error>`
600-
601-
## Migration Notes
602-
603-
> This section describes how this pattern differs from older code in the
604-
> codebase. New code MUST follow the patterns above. When modifying existing
605-
> files, update them to match these patterns where practical.
606-
607-
Some existing form components use `useEffect` to synchronize form state
608-
with loaded data (e.g., `EventModal.tsx` uses `useEffect` to populate
609-
Autocomplete selections from `initialValues` when `users` data loads).
610-
The prescribed pattern avoids this entirely by computing all initial
611-
values in the `defaultValues` object passed to `useForm`. When modifying
612-
these files, replace `useEffect`-based synchronization with proper
613-
`defaultValues` computation or direct `reset()` calls in event handlers.
614-
615-
Some older form modals also maintain parallel `useState` for values that
616-
should be managed by React Hook Form (e.g., separate `useState` for
617-
selected members alongside the form's member IDs). New forms MUST keep
618-
all form state within React Hook Form — use `watch()` to read values
619-
and `setValue()` to write them programmatically when needed.

0 commit comments

Comments
 (0)