@atproto/lex-server

中文文档
0.2.3发布于 2026/11/06MITTypeScriptES Module构建来源已验证近 30 天下载 6645

Request router for Atproto Lexicon protocols and schemas

原始使用文档
文档目录28 节
  1. Quick Start
  2. LexRouter
  3. Creating a Router
  4. Adding Routes
  5. Handler Context
  6. Handler Output
  7. Queries and Procedures
  8. Query Handler
  9. Procedure Handler
  10. Binary Payloads
  11. Subscriptions
  12. Authentication
  13. Custom Authentication
  14. WWW-Authenticate Headers
  15. Error Handling
  16. LexServerError
  17. LexServerAuthError
  18. Error Handler Callback
  19. Node.js Server
  20. serve()
  21. createServer()
  22. toRequestListener()
  23. upgradeWebSocket()
  24. Advanced Usage
  25. Custom Response Objects
  26. Response Headers
  27. Connection Info
  28. License

@atproto/lex-server

Request router for Atproto Lexicon protocols and schemas. See the Changelog for version history.

npm install @atproto/lex-server
  • Type-safe request routing based on Lexicon schemas
  • Support for queries, procedures, and WebSocket subscriptions
  • Built on Web standard Request/Response APIs (portable across runtimes)
  • Custom authentication with credential passing
  • Graceful shutdown with AsyncDisposable pattern

[!IMPORTANT]

This package is currently in preview. The API and features are subject to change before the stable release.

What is this?

Building AT Protocol servers requires handling XRPC requests, validating inputs against Lexicon schemas, managing authentication, and supporting real-time subscriptions. @atproto/lex-server automates this by:

  1. Routing requests to type-safe handlers based on Lexicon schemas
  2. Automatically validating request parameters and bodies
  3. Providing a flexible authentication system with custom strategies
  4. Supporting WebSocket subscriptions with backpressure handling
import { LexRouter } from '@atproto/lex-server'
import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
import * as app from './lexicons/app.js'

const router = new LexRouter({ upgradeWebSocket })
  .add(app.bsky.actor.getProfile, async ({ params }) => {
    const profile = await db.getProfile(params.actor)
    return { body: profile }
  })
  .add(app.bsky.feed.post.create, {
    auth: requireAuth,
    handler: async ({ credentials, input }) => {
      const result = await db.createPost(credentials.did, input.body)
      return { body: result }
    },
  })

await serve(router, { port: 3000 })

Quick Start

1. Install the package

npm install @atproto/lex-server

2. Generate Lexicon schemas

Use @atproto/lex to generate TypeScript schemas from your Lexicon definitions:

lex install app.bsky.actor.getProfile
lex build

3. Create a router and add handlers

import { LexRouter, LexServerError } from '@atproto/lex-server'
import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
import * as app from './lexicons/app.js'

const router = new LexRouter({ upgradeWebSocket })

// Add a query handler
router.add(app.bsky.actor.getProfile, async ({ params }) => {
  const profile = await db.getProfile(params.actor)
  if (!profile) {
    throw new LexServerError(404, {
      error: 'NotFound',
      message: 'Profile not found',
    })
  }
  return { body: profile }
})

// Start the server
const server = await serve(router, { port: 3000 })
console.log('Server listening on port 3000')

LexRouter

The LexRouter class is the core of @atproto/lex-server. It routes XRPC requests to type-safe handlers based on Lexicon schemas.

Creating a Router

import { LexRouter } from '@atproto/lex-server'
import { upgradeWebSocket } from '@atproto/lex-server/nodejs'

const router = new LexRouter({
  // Required for WebSocket subscriptions (Node.js)
  upgradeWebSocket,

  // Optional: Add logging or error reporting for unexpected errors in handlers
  onHandlerError: ({ error, request, method }) => {
    console.error(`Error in ${method.nsid}:`, error)
  },

  // Optional: WebSocket backpressure settings
  highWaterMark: 250_000, // bytes (default: 250KB)
  lowWaterMark: 50_000, // bytes (default: 50KB)
})

Adding Routes

Routes are added using the .add() method, which accepts a Lexicon schema and a handler:

// Simple handler (no authentication)
router.add(schema, async ({ params, input, request }) => {
  return { body: result }
})

// Handler with authentication
router.add(schema, {
  auth: async ({ request, params }) => credentials,
  handler: async ({ params, input, credentials, request }) => {
    return { body: result }
  },
})

The router supports method chaining:

const router = new LexRouter()
  .add(app.bsky.actor.getProfile, profileHandler)
  .add(app.bsky.feed.getTimeline, timelineHandler)
  .add(app.bsky.feed.post.create, postHandler)

Handler Context

Handlers receive a context object with the following properties:

type LexRouterHandlerContext<Method, Credentials> = {
  credentials: Credentials // Result of auth function (undefined if no auth)
  input: InferMethodInput<Method> // Parsed request body (procedures only)
  params: InferMethodParams<Method> // Parsed URL query parameters
  request: Request // Original Web Request object
  connection?: ConnectionInfo // Network connection info
}

Handler Output

Handlers can return various output formats:

// JSON response (encoding inferred from schema)
return { body: { key: 'value' } }

// With custom encoding
return { encoding: 'text/plain', body: 'Hello, world!' }

// With response headers
return { body: data, headers: { 'x-custom': 'value' } }

// Empty response (200 OK with no body)
return {}

// Custom Response object (full control)
return new Response(body, { status: 201, headers })

// Proxy Response
return fetch('https://example.com/data')

Queries and Procedures

Query Handler

Queries handle GET requests and receive parameters from the URL query string:

import * as app from './lexicons/app.js'

router.add(app.bsky.actor.getProfile, async ({ params }) => {
  // params.actor is typed and validated
  const profile = await db.getProfile(params.actor)
  return { body: profile }
})

Procedure Handler

Procedures handle POST requests and receive a request body:

router.add(app.bsky.feed.post.create, async ({ input }) => {
  // input.body contains the parsed and validated request body
  const post = await db.createPost(input.body)
  return { body: { uri: post.uri, cid: post.cid } }
})

Binary Payloads

For endpoints that accept or return binary data:

// Binary input
router.add(app.example.uploadBlob, async ({ input }) => {
  // input.body is a Request object for streaming
  // input.encoding contains the content-type
  const blob = await input.body.arrayBuffer()
  return { body: { cid: await store(blob) } }
})

// Binary output
router.add(app.example.getBlob, async ({ params }) => {
  const stream = await getBlob(params.cid)
  return {
    encoding: 'application/octet-stream',
    body: stream,
  }
})

Subscriptions

Subscriptions provide real-time data over WebSocket connections. Handlers are async generators that yield messages:

import { LexRouter, LexError } from '@atproto/lex-server'
import { serve, upgradeWebSocket } from '@atproto/lex-server/nodejs'
import { scheduler } from 'node:timers/promises'

const router = new LexRouter({
  upgradeWebSocket, // Required for WebSocket support in nodejs
})

router.add(com.example.stream, async function* ({ params, request }) {
  const { cursor = 0, limit = 10 } = params
  const { signal } = request

  for (let i = 0; i < limit; i++) {
    // Yield messages to the client
    yield com.example.stream.message.$build({
      data: `Message ${cursor + i}`,
      cursor: cursor + i,
    })

    // Wait between messages (respects abort signal)
    await scheduler.wait(1000, { signal })
  }

  // Throwing a LexError closes the connection with an error frame
  throw new LexError('LimitReached', `Limit of ${limit} messages reached`)
})

Messages are CBOR-encoded and sent as WebSocket binary frames. The router handles:

  • WebSocket upgrade negotiation
  • Backpressure management
  • Graceful connection cleanup
  • Error frame encoding

Authentication

Custom Authentication

Authentication is implemented through the auth function in handler configs:

import { LexError, LexServerAuthError } from '@atproto/lex-server'

type Credentials = { did: string; scope: string[] }

const requireAuth = async ({
  request,
}: {
  request: Request
}): Promise<Credentials> => {
  const header = request.headers.get('authorization')
  if (!header?.startsWith('Bearer ')) {
    throw new LexServerAuthError(
      'AuthenticationRequired',
      'Bearer token required',
      {
        Bearer: { realm: 'api' },
      },
    )
  }

  const token = header.slice(7)
  const session = await verifyToken(token)
  if (!session) {
    throw new LexServerAuthError(
      'InvalidToken',
      'Token is invalid or expired',
      {
        Bearer: { realm: 'api', error: 'invalid_token' },
      },
    )
  }

  return { did: session.did, scope: session.scope }
}

// Use with handlers
router.add(app.bsky.feed.post.create, {
  auth: requireAuth,
  handler: async ({ credentials, input }) => {
    // credentials.did is available here
    const post = await db.createPost(credentials.did, input.body)
    return { body: post }
  },
})

The auth function:

  1. Is called before parsing the request body
  2. Receives params, request, and connection info
  3. Should throw LexServerError (or LexServerAuthError) on failure
  4. Returns credentials that are passed to the handler

WWW-Authenticate Headers

Use LexServerAuthError to include WWW-Authenticate headers in error responses:

import { LexServerAuthError } from '@atproto/lex-server'

// Simple Bearer challenge
throw new LexServerAuthError('AuthenticationRequired', 'Login required', {
  Bearer: { realm: 'api' },
})
// WWW-Authenticate: Bearer realm="api"

// Multiple schemes
throw new LexServerAuthError('AuthenticationRequired', 'Auth required', {
  Bearer: { realm: 'api', scope: 'read write' },
  Basic: { realm: 'api' },
})
// WWW-Authenticate: Bearer realm="api", scope="read write", Basic realm="api"

// Token68 format
throw new LexServerAuthError('AuthenticationRequired', 'Auth required', {
  Bearer: 'token68value',
})
// WWW-Authenticate: Bearer token68value

Error Handling

LexServerError

Throw LexServerError to return structured XRPC error responses:

import { LexServerError } from '@atproto/lex-server'

router.add(app.bsky.actor.getProfile, async ({ params }) => {
  try {
    const profile = await db.getProfile(params.actor)
    return { body: profile }
  } catch (cause) {
    throw new LexServerError(
      404,
      {
        error: 'NotFound',
        message: 'Profile not found',
      },
      {
        'cache-control': 'max-age=600',
      },
      { cause },
    )
  }
})

Error responses follow the XRPC format:

{
  "error": "NotFound",
  "message": "Profile not found"
}

LexServerAuthError

LexServerAuthError extends LexServerError with WWW-Authenticate header support:

import { LexServerAuthError } from '@atproto/lex-server'

throw new LexServerAuthError('AuthenticationRequired', 'Invalid credentials', {
  Bearer: { realm: 'api' },
})

This returns a 401 response with the WWW-Authenticate header.

Error Handler Callback

Use onHandlerError to log or report unexpected errors:

const router = new LexRouter({
  onHandlerError: async ({ error, request, method }) => {
    // Log errors (excluding expected abort signals)
    console.error(`Error in ${method.nsid}:`, error)
    await reportToSentry(error)
  },
})

[!NOTE]

The callback is only invoked for unexpected errors, not for LexError instances or request aborts.

Node.js Server

The @atproto/lex-server/nodejs subpath provides Node.js-specific utilities.

serve()

Start a server and begin listening:

import { serve } from '@atproto/lex-server/nodejs'

const server = await serve(router, { port: 3000 })
console.log('Server listening on port 3000')

// Graceful shutdown
await server.terminate()

The server supports AsyncDisposable:

await using server = await serve(router, { port: 3000 })
// Server is automatically terminated when scope exits

Options:

type StartServerOptions = {
  port?: number
  host?: string
  gracefulTerminationTimeout?: number // ms to wait for connections to close
}

createServer()

Create a server without starting it:

import { createServer } from '@atproto/lex-server/nodejs'

const server = createServer(router, {
  gracefulTerminationTimeout: 5000,
})

server.listen(3000, () => {
  console.log('Server listening')
})

toRequestListener()

Convert a handler to an Express/Connect-compatible middleware:

import express from 'express'
import { toRequestListener } from '@atproto/lex-server/nodejs'

const app = express()

// Mount the XRPC router
app.use(toRequestListener(router.fetch))

app.listen(3000)

upgradeWebSocket()

Required for WebSocket subscription support in Node.js:

import { LexRouter } from '@atproto/lex-server'
import { upgradeWebSocket } from '@atproto/lex-server/nodejs'

const router = new LexRouter({ upgradeWebSocket })

Advanced Usage

Custom Response Objects

Return a Response object for full control over the response (disables any kind of validation of the body):

router.add(schema, async ({ params }) => {
  if (params.redirect) {
    return Response.redirect('https://example.com', 302)
  }

  return Response.json(
    { custom: true },
    {
      status: 201,
      headers: {
        'X-Custom-Header': 'value',
      },
    },
  )
})

Response Headers

Add headers to responses:

router.add(schema, async ({ params }) => {
  return {
    body: { data: 'value' },
    headers: {
      'Cache-Control': 'public, max-age=3600',
      'X-Request-Id': crypto.randomUUID(),
    },
  }
})

Connection Info

Access network connection information:

router.add(schema, async ({ connection }) => {
  console.log('Remote address:', connection?.remoteAddr?.hostname)
  console.log('Local address:', connection?.localAddr?.hostname)
  return { body: { status: 'ok' } }
})

Connection info structure:

type ConnectionInfo = {
  localAddr?: {
    hostname: string
    port: number
    transport: 'tcp' | 'udp'
  }
  remoteAddr?: {
    hostname: string
    port: number
    transport: 'tcp' | 'udp'
  }
}

License

MIT or Apache2

版本与发布记录

@atproto/lex-server 共发布 55 个版本,首次发布于 2026/01/02;过去 12 个月发布了 55 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.2.3latest2026/11/061 天前
0.0.0-spaces-alpha-20261105160032alpha2026/11/062 天前
0.0.0-spaces-alpha-202610291719132026/10/309 天前
0.0.0-spaces-alpha-202610221412282026/10/2216 天前
0.2.02026/10/1721 天前
0.0.0-spaces-alpha-202610151435092026/10/1523 天前

其他发布通道:next → 0.1.0-next.0,alpha → 0.0.0-spaces-alpha-20261105160032。

关于 @atproto/lex-server 的常见问题

@atproto/lex-server 是什么?

Request router for Atproto Lexicon protocols and schemas

如何安装 @atproto/lex-server?

在项目目录运行 npm install @atproto/lex-server,或使用 pnpm add / yarn add / bun add。

@atproto/lex-server 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@atproto/lex-server 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@atproto/lex-server 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@atproto/lex-server 可以商用吗?

@atproto/lex-server 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@atproto/lex-server 的下载量有多少?

根据 npm 官方下载统计,@atproto/lex-server 在 2026/10/07 – 2026/11/05 这 30 天内被下载 6,645 次,日均约 222 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@atproto/lex-server 还在维护吗?

最新版本 0.2.3 发布于 2026/11/06(1 天前);至今共发布 55 个版本,其中过去 12 个月发布了 55 个;首次发布于 2026/01/02。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。