Developing with Lexicons

Using Lexicons in your dev environment

Lexicon schemas drive code generation, request validation, and client typing across the AT Protocol stack.

Installing Lexicons with lex

Our SDKs each install a lex tool that lets you generate a type-safe client that knows which parameters each AT endpoint expects.

TypeScript

First, install the @atproto/lex package:

npm install -g @atproto/lex

This provides the lex command, which you can use to install Lexicons into a local project:

lex install app.bsky.feed.post app.bsky.feed.like

This creates:

  • lexicons.json - manifest tracking installed Lexicons and their versions (CIDs)
  • lexicons/ - directory containing the Lexicon JSON files

Finally, generate TypeScript schemas from the installed Lexicons:

lex build

This writes TypeScript files to ./src/lexicons. Once you've run lex build, calling a method is a typed client.call against the generated definition:

import { Client } from '@atproto/lex'
import * as app from './lexicons/app.js'

// Create an unauthenticated client instance
const client = new Client('https://public.api.bsky.app')

// Start making requests using generated schemas
const response = await client.call(app.bsky.actor.getProfile, {
  actor: 'pfrazee.com',
})

For more guidance on working with lex, refer to the readme.

XRPC Methods

A Lexicon whose main definition is a method describes one of three kinds of XRPC call:

  • query — a read, served over HTTP GET, parameters in the query string. See Reading Data for end-to-end examples.
  • procedure — a side-effectful call, served over HTTP POST, input as a JSON body (or another MIME type if declared). See Writing Data.
  • subscription — a server-pushed WebSocket stream with DAG-CBOR frames, used for the firehose.

Each method is reachable at /xrpc/<nsid> on whatever service implements it — app.bsky.actor.getProfile lives at https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile. The NSID is the URL path.

Parameters, input body, and output are all type-checked at compile time and validated at runtime against the Lexicon shape.

Using the .internal TLD

NSIDs use reverse DNS lookup — app.bsky.* resolves under bsky.app, com.atproto.* under atproto.com. But for service-to-service calls between components of the same operator, there's no public schema to publish or resolve.

For those, you can use NSIDs prefixed with internal., taking advantage of .internal being a reserved TLD that can never be registered. For example, the reference implementation includes internal.bsky.actor.getProfiles, a variant of app.bsky.actor.getProfiles.

This gives you:

  • Built-in SDK exclusion. The codegen filter in @atproto/lex treats *.internal.* as the canonical exclude pattern, so internal endpoints never appear in your published client SDK.
  • All of the XRPC machinery. Same codegen, typed handlers, request/response validation, inter-service auth, and /xrpc/<nsid> routing, but without committing the endpoint to your public protocol surface.

Use internal.* whenever an XRPC method is plumbing for your own infrastructure: optimizations specific to one caller, admin or operational queries, or anything you wouldn't want a third-party implementer to feel obligated to support.

Further Reading and Resources