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 nestjsExpress
Router methods, middleware
--framework expressFastify
Route definitions, schemas
--framework fastifyHono
Route handlers, validators
--framework honoElysia
Type-safe routes, schemas
--framework elysiatRPC
Procedure definitions
--framework trpcNext.js
API routes (app/api, pages/api)
--framework next-api1) Quick start
Run the import command to auto-detect your framework and extract endpoints:
# 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 nestjsExpected 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
@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
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
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:
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
| Option | Description | Example |
|---|---|---|
--scope | Limit to specific directories | --scope src/users src/auth |
--framework | Force a specific framework | --framework express |
--output | Output directory | --output ./contracts |
--dry-run | Preview without writing files | --dry-run |
--analyze | Analysis only, no code generation | --analyze |
--json | Output as JSON for scripting | --json |
# 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.json5) Registering imported contracts
After import, register contracts and add handlers:
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
- Review generated contracts — Check the TODO placeholders and fill in descriptions, owners, and tags.
- Refine schemas — Add proper types, validation rules, and error definitions.
- Run validation —
contractspec validateto ensure contracts are valid. - Register and wire handlers — Connect contracts to your existing business logic.
- 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