Skip to content
Back to skills

Feathers Schema

ASecurity

Define, validate, and secure a FeathersJS v5 data model with TypeBox schemas and the four resolver kinds (result, data, query, external). Use whenever the user is working on a Feathers <name>.schema.ts file, adding or changing a field, deriving TypeScript types from a schema, validating request data or query strings, hiding sensitive fields (like passwords) from API responses, populating associations / virtual properties, hashing passwords, setting defaults like createdAt, or restricting whic...

  • 2 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added June 2, 2026
developmenttypescriptrustgosqlapidatabasesecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned June 2, 2026

npx -y skills add hassan4702/feathers-plugin --skill feathers-schema --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Feathers Schema?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Feathers Schema
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/hassan4702-feathers-schema/badge)](https://www.skillsdirectory.com/skills/hassan4702-feathers-schema)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: feathers-schema
description: Define, validate, and secure a FeathersJS v5 data model with TypeBox schemas and the four resolver kinds (result, data, query, external). Use whenever the user is working on a Feathers <name>.schema.ts file, adding or changing a field, deriving TypeScript types from a schema, validating request data or query strings, hiding sensitive fields (like passwords) from API responses, populating associations / virtual properties, hashing passwords, setting defaults like createdAt, or restricting which records a user can query. Trigger on mentions of TypeBox, Type.Object, getValidator, resolve(), querySyntax, Static, $id, "validate", "schema", or "resolver" in a Feathers context.
---

# FeathersJS Schemas & Resolvers

Schemas and resolvers are database-independent. A **schema** (TypeBox) defines shape, derives the TS type, and validates. A **resolver** transforms property values against the hook context. They are applied through the `schemaHooks` registered in the service file (see `feathers-service`).

## The four kinds — what each is for

- **Result** (`<name>Resolver`): shapes data being returned; the place to populate associations / virtual properties.
- **Data** (`<name>DataResolver`): runs on `create`/`update`/`patch` data before saving; set defaults, computed values, hash passwords, stamp `createdAt`.
- **Query** (`<name>QueryResolver`): validates/coerces the query string and is the right place to **restrict which records a user may touch** (row-level security).
- **External** (`<name>ExternalResolver`): produces the safe, outward-facing version — **hide sensitive fields here** (e.g. `password`).

## Defining the schema

```ts
import { resolve, virtual } from '@feathersjs/schema'
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'
import type { Static } from '@feathersjs/typebox'
import type { HookContext } from '../../declarations'
import { dataValidator, queryValidator } from '../../validators'

export const messageSchema = Type.Object(
  {
    id: Type.Number(),            // MongoDB: _id: ObjectIdSchema()
    text: Type.String(),
    createdAt: Type.Number(),
    userId: Type.Number(),
    user: Type.Ref(userSchema)    // associated, populated by a virtual resolver
  },
  { $id: 'Message', additionalProperties: false }
)
export type Message = Static<typeof messageSchema>   // type derived from schema — single source of truth
export const messageValidator = getValidator(messageSchema, dataValidator)
```

`$id` must be globally unique. `additionalProperties: false` rejects unknown fields. Get the TS type with `Static<typeof ...>` — never hand-maintain a parallel interface.

Useful TypeBox helpers: `Type.Optional(...)`, `Type.Pick(schema, [...])`, `Type.Partial(schema)` (for patch), `Type.Intersect([...])`, `Type.Ref(otherSchema)` for associations.

## Result resolver + virtual (populate an association)

```ts
export const messageResolver = resolve<Message, HookContext>({
  user: virtual(async (message, context) => {
    return context.app.service('users').get(message.userId)
  })
})
```

`virtual()` marks a property whose value does not come from the table/collection.

## Data resolver (defaults, computed, association from auth)

```ts
export const messageDataSchema = Type.Pick(messageSchema, ['text'], { $id: 'MessageData' })
export type MessageData = Static<typeof messageDataSchema>
export const messageDataValidator = getValidator(messageDataSchema, dataValidator)
export const messageDataResolver = resolve<Message, HookContext>({
  userId: async (_value, _message, context) => context.params.user.id, // stamp owner from the JWT, never trust client
  createdAt: async () => Date.now()
})
```

Deriving `userId` from `context.params.user` (not from request body) is the correct way to prevent a client from spoofing ownership.

## External resolver (hide sensitive data)

```ts
export const userExternalResolver = resolve<User, HookContext>({
  password: async () => undefined   // never leaves the server
})
```

## Query resolver (row-level security)

```ts
export const messageQueryProperties = Type.Pick(messageSchema, ['id', 'text', 'createdAt', 'userId'])
export const messageQuerySchema = Type.Intersect(
  [querySyntax(messageQueryProperties), Type.Object({}, { additionalProperties: false })],
  { additionalProperties: false }
)
export type MessageQuery = Static<typeof messageQuerySchema>
export const messageQueryValidator = getValidator(messageQuerySchema, queryValidator)
export const messageQueryResolver = resolve<MessageQuery, HookContext>({
  userId: async (value, _user, context) => {
    // allow find-all, but force write ops to the caller's own rows
    if (context.params.user && context.method !== 'find') {
      return context.params.user.id
    }
    return value
  }
})
```

`querySyntax(props)` adds Feathers' query operators (`$gt`, `$in`, `$sort`, `$limit`, etc.) for the listed properties only — anything not listed is rejected, which is itself a security boundary.

## Password hashing

```ts
import { passwordHash } from '@feathersjs/authentication-local'
export const userDataResolver = resolve<User, HookContext>({
  password: passwordHash({ strategy: 'local' })
})
```

## Migrations follow schema changes (SQL only)

Every schema field change needs a matching Knex migration: `npm run migrate:make -- <name>`, edit `up`/`down` (`table.string(...)`, `table.bigint(...).references('id').inTable('users')`), then `npm run migrate`. MongoDB needs none.

## Pitfalls

- Adding a field to the schema but forgetting the migration → runtime/DB errors.
- Reusing a `$id` across services → validator collisions.
- Putting authorization in the data resolver instead of the query resolver → `find`/`get`/`remove` aren't protected.
- Forgetting the external resolver → passwords and secrets leak to clients.
- Defining resolvers but not wiring the matching `schemaHooks` in the service file → they never run.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.