new-service · git:20260430.17a2444 · 2026-04-30 · sha256 177f7f4300c466c5
new-service git:20260430.17a2444A
Immutable. This exact content is served forever at /api/v1/blob/177f7f4300c466c5.
---
name: new-service
description: Scaffold a new API service with request functions, Zod schemas, URL constants, and optional MSW mock handlers. Use this skill whenever the user asks to "add a service", "create the projects API", "scaffold a new GAIA service", "wire up CRUD for users", or anything implying a new folder under `app/services/gaia/{name}/` with parsers/types/requests + matching `test/mocks/{name}/` collections.
---
Scaffold a new API service with request functions, Zod schemas, URL constants, and optional MSW mock handlers.
## Step 1: Gather user input
Ask the user these questions using AskUserQuestion:
- Service name (e.g. `projects`, `users`, `orders`)
- API endpoints — list of method + path pairs (e.g. `GET /projects`, `GET /projects/:id`, `POST /projects`, `PUT /projects/:id`, `DELETE /projects/:id`)
- Describe the schema fields for the resource (e.g. `id: string, name: string, createdAt: datetime`)
- Does it need MSW mock handlers? (yes/no) — default yes
## Step 2: Derive names
From the service name, derive:
- **serviceDir**: `app/services/gaia/{serviceName}/`
- **urlKeys**: camelCase keys for each endpoint path (e.g. `projects`, `projectsId`)
- **schemaName**: singular form + `Schema` (e.g. `projectSchema`)
- **listSchemaName**: plural form + `Schema` (e.g. `projectsSchema`)
- **typeName**: singular PascalCase (e.g. `Project`)
- **listTypeName**: plural PascalCase (e.g. `Projects`)
## Step 3: Add URL constants
Edit `app/services/gaia/urls.ts` — add new URL keys to `GAIA_URLS`, maintaining alphabetical order:
```ts
export const GAIA_URLS = {
projects: 'projects', // new
projectsId: 'projects/:id', // new
};
```
## Step 4: Create schema parsers
Create `app/services/gaia/{serviceName}/parsers.ts` per [[API Service Pattern]]:
```ts
import {z} from 'zod';
export const {schemaName} = z.object({
// fields from user input, using appropriate Zod types
// z.string(), z.iso.datetime(), z.number(), z.boolean(), etc.
});
export const {listSchemaName} = z.array({schemaName});
```
## Step 5: Create types
Create `app/services/gaia/{serviceName}/types.ts`:
```ts
import type {z} from 'zod';
import type {{schemaName}, {listSchemaName}} from './parsers';
export type {TypeName} = z.infer<typeof {schemaName}>;
export type {ListTypeName} = z.infer<typeof {listSchemaName}>;
```
## Step 6: Create request functions
Create `app/services/gaia/{serviceName}/requests.server.ts` per [[API Service Pattern]]:
```ts
import {api} from '../api';
import {GAIA_URLS} from '../urls';
import {{schemaName}, {listSchemaName}} from './parsers';
import type {{TypeName}, {ListTypeName}} from './types';
```
Generate one function per endpoint:
- **GET (list)**: `getAll{PluralName}` — uses `listSchemaName.parse(result.data)`
- **GET (single)**: `get{SingularName}ById` — uses `schemaName.parse(result.data)`, accepts `id: string`
- **POST**: `create{SingularName}` — accepts `body: FormData`, uses `schemaName.parse(result.data)`
- **PUT**: `update{SingularName}` — accepts `body: FormData`, uses `pathParams: {id: body.get('id')}`
- **DELETE**: `delete{SingularName}` — accepts `id: string`, returns `ReturnType<typeof api>`
## Step 7: Update barrel export
Edit `app/services/gaia/index.server.ts` — add the new service import and export, maintaining alphabetical order:
```ts
import * as {serviceName} from './{serviceName}/requests.server';
```
## Step 8: Create MSW mocks (if requested)
### 8a: Create mock data
Create `test/mocks/{serviceName}/data.ts` per [[MSW]]. The `Collection` takes a Standard Schema (Zod) directly — there is no parallel `@msw/data` schema to maintain.
```ts
import {Collection} from '@msw/data';
import {z} from 'zod';
export const server{TypeName}Schema = z.object({
// snake_case server versions of the schema fields
});
export type Server{TypeName} = z.infer<typeof server{TypeName}Schema>;
export const {serviceName} = new Collection({schema: server{TypeName}Schema});
const seed: Server{TypeName}[] = [
// 2 sample records
];
export const reset{PluralTypeName} = async (): Promise<void> => {
await {serviceName}.deleteMany((q) => q);
for (const record of seed) {
await {serviceName}.create(record);
}
};
```
### 8b: Create mock handlers
Create one file per HTTP method in `test/mocks/{serviceName}/`. Reads on a `Collection` are sync; mutations are async.
- `get.ts` — `http.get` + `{serviceName}.findMany(undefined)` (list) or `{serviceName}.findFirst((q) => q.where({id: params.id}))` (single)
- `post.ts` — `http.post`, `request.formData()`, `await {serviceName}.create({...})`
- `put.ts` — `http.put`, `request.formData()`, `await {serviceName}.update((q) => q.where({id: params.id}), {data(rec) { rec.field = value; }})`
- `delete.ts` — `http.delete`, `await {serviceName}.delete((q) => q.where({id: params.id}))`
Import the collection from the domain's data file:
```ts
import {{serviceName}} from './data';
```
Each handler uses `${process.env.API_URL}${GAIA_URLS.{urlKey}}` for the URL.
### 8c: Create mock barrel
Create `test/mocks/{serviceName}/index.ts`:
```ts
import del from './delete';
import get from './get';
import post from './post';
import put from './put';
export default [...get, post, put, del];
```
Only include the methods that match the user's requested endpoints.
### 8d: Update mock database
Edit `test/mocks/database.ts`:
- Import the new collection and its `reset*` from `./{serviceName}/data`
- Add the collection to the default-export object so `database.{serviceName}` resolves
- Await `reset*` inside `resetTestData` so every collection wipes and re-seeds together
Example after adding `things`:
```ts
import {things, resetThings} from './things/data';
export const resetTestData = async (): Promise<void> => {
await Promise.all([resetThings()]);
};
export default {things};
```
### 8e: Update mock barrel
Edit `test/mocks/index.ts`:
- Add import for the new mock handlers
- Spread into the handlers array
## Step 9: Verify
Run these commands sequentially, stopping if any fails:
```bash
pnpm typecheck && pnpm lint && pnpm test --run
```
Fix any issues before reporting to the user.