Import Existing Codebases

Convert your existing API endpoints into ContractSpec contracts. Auto-detect frameworks, extract schemas, and generate spec-first definitions from your code.

What you'll learn

  • How to import existing API endpoints as ContractSpec contracts.
  • Framework-specific patterns and auto-detection.
  • Customizing imports with scopes, dry-run, and output options.
  • Registering imported contracts and adding handlers.

Why import existing code?

Instead of writing contracts from scratch, the import command extracts endpoint patterns from your existing codebase. This gives you:

  • Instant spec coverage for existing APIs
  • Type-safe schemas derived from your existing types
  • A foundation to iterate and refine contracts
  • Gradual adoption without rewriting code

Supported frameworks

NestJS

Controllers, decorators, DTOs

--framework nestjs

Express

Router methods, middleware

--framework express

Fastify

Route definitions, schemas

--framework fastify

Hono

Route handlers, validators

--framework hono

Elysia

Type-safe routes, schemas

--framework elysia

tRPC

Procedure definitions

--framework trpc

Next.js

API routes (app/api, pages/api)

--framework next-api

1) Quick start

Run the import command to auto-detect your framework and extract endpoints:

import-quickstart
# Auto-detect framework and import all endpoints
contractspec import ./src

# Preview what would be imported (dry-run)
contractspec import ./src --dry-run

# Force a specific framework
contractspec import ./src --framework nestjs

Expected output: A summary of endpoints found, schemas extracted, and files generated.

2) Framework-specific patterns

The import command recognizes these patterns in each framework:

NestJS

src/users/users.controller.ts (before)
@Controller('users')
export class UsersController {
  @Get(':id')
  async getUser(@Param('id') id: string): Promise<UserDto> {
    return this.usersService.findOne(id);
  }

  @Post()
  @UseGuards(AuthGuard)
  async createUser(@Body() dto: CreateUserDto): Promise<UserDto> {
    return this.usersService.create(dto);
  }
}

Detected: @Controller, @Get, @Post, @Body, @Param, @UseGuards decorators.

Express

src/routes/users.ts (before)
const router = express.Router();

router.get('/users/:id', async (req, res) => {
  const user = await getUserById(req.params.id);
  res.json(user);
});

router.post('/users', validateBody(CreateUserSchema), async (req, res) => {
  const user = await createUser(req.body);
  res.status(201).json(user);
});

Detected: router.get, router.post, validation middleware, Zod schemas.

Next.js API Routes

app/api/users/[id]/route.ts (before)
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const user = await getUserById(params.id);
  return Response.json(user);
}

export async function POST(request: Request) {
  const body = await request.json();
  const user = await createUser(body);
  return Response.json(user, { status: 201 });
}

Detected: GET, POST exports in app/api/**/route.ts and pages/api/**/*.ts.

3) Understanding generated contracts

The import command generates ContractSpec operations with TODO placeholders for fields it cannot infer:

.contractspec/generated/users.operation.ts
import { defineQuery, defineCommand } from "@contractspec/lib.contracts";
import { SchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";

export const GetUserQuery = defineQuery({
  meta: {
    key: "users.get",
    version: "1.0.0",
    description: "TODO: Add description",
    stability: "draft",
    owners: ["TODO"],
    tags: ["users"],
  },
  io: {
    input: new SchemaModel({
      name: "GetUserInput",
      fields: {
        id: { type: ScalarTypeEnum.String(), isOptional: false },
      },
    }),
    output: new SchemaModel({
      name: "GetUserOutput",
      fields: {
        // TODO: Add output fields based on your UserDto
        id: { type: ScalarTypeEnum.String(), isOptional: false },
      },
    }),
  },
  policy: { auth: "user" }, // Inferred from @UseGuards
  transport: {
    rest: { method: "GET", path: "/users/:id" },
  },
});

export const CreateUserCommand = defineCommand({
  meta: {
    key: "users.create",
    version: "1.0.0",
    description: "TODO: Add description",
    stability: "draft",
    owners: ["TODO"],
    tags: ["users"],
  },
  io: {
    input: new SchemaModel({
      name: "CreateUserInput",
      fields: {
        // TODO: Add fields from CreateUserDto
      },
    }),
    output: new SchemaModel({
      name: "CreateUserOutput",
      fields: {
        id: { type: ScalarTypeEnum.String(), isOptional: false },
      },
    }),
  },
  policy: { auth: "admin" },
  transport: {
    rest: { method: "POST", path: "/users" },
  },
});

4) Customizing imports

Available options

OptionDescriptionExample
--scopeLimit to specific directories--scope src/users src/auth
--frameworkForce a specific framework--framework express
--outputOutput directory--output ./contracts
--dry-runPreview without writing files--dry-run
--analyzeAnalysis only, no code generation--analyze
--jsonOutput as JSON for scripting--json
import-options
# Import only specific modules
contractspec import ./src --scope src/users src/orders

# Preview with analysis
contractspec import ./src --dry-run --analyze

# Output to custom directory
contractspec import ./src --output ./src/contracts/generated

# Get JSON for CI/scripting
contractspec import ./src --json > import-result.json

5) Registering imported contracts

After import, register contracts and add handlers:

src/contracts/registry.ts
import {
  OperationSpecRegistry,
  installOp,
} from "@contractspec/lib.contracts/operations";
import {
  GetUserQuery,
  CreateUserCommand,
} from "./.contractspec/generated/users.operation";

export const registry = new OperationSpecRegistry();

// Add handlers to imported operations
installOp(registry, GetUserQuery, async (input) => {
  const user = await db.user.findUnique({ where: { id: input.id } });
  return user;
});

installOp(registry, CreateUserCommand, async (input) => {
  const user = await db.user.create({ data: input });
  return { id: user.id };
});

6) After importing

  1. Review generated contracts — Check the TODO placeholders and fill in descriptions, owners, and tags.
  2. Refine schemas — Add proper types, validation rules, and error definitions.
  3. Run validation contractspec validate to ensure contracts are valid.
  4. Register and wire handlers — Connect contracts to your existing business logic.
  5. Iterate — The imported contracts are a starting point. Refine them as your spec matures.

Troubleshooting

Framework not detected

Use --framework <name> to force a specific framework. Check that your entry files follow standard patterns.

Missing schemas

Schema inference works best with explicit types. If using any or dynamic types, you'll see TODO placeholders. Fill them in manually.

Partial imports

Some endpoints may not be detected if they use unconventional patterns. Use --analyze to see what was found, then add missing contracts manually.

Want automated contract evolution?

Studio monitors your codebase and suggests contract updates when your API changes, keeping specs and code in sync.

Join Studio waitlist