Code Generation and Templates
Overview
Code generation transforms machine-readable specifications (OpenAPI, GraphQL schemas, database schemas) into executable code. This eliminates the error-prone manual translation step where developers transcribe API contracts into DTOs, or database schemas into entity classes. The compiler enforces consistency - when the specification changes, the generated code changes, causing compilation errors in any code that depends on outdated assumptions. This makes breaking changes visible at build time rather than runtime.
Schema-first development inverts the traditional code-then-document workflow. Instead of writing implementation code first and documenting it afterward, you define the contract (API schema, database schema) as the authoritative source. Code generation then produces implementation scaffolding from this single source of truth. This approach prevents documentation drift (where docs and code diverge), enables parallel development (frontend and backend teams work from the same contract simultaneously), and catches integration issues through contract validation before any integration testing occurs.
Core Principles
- Generate, Don't Handwrite: Repetitive code like DTOs, API clients, and database entities should be auto-generated from specifications
- Single Source of Truth: The schema or contract is the authoritative definition; generated code is derived, not edited
- Regenerate on Change: Specifications evolve; regenerating code ensures changes propagate to all consumers immediately
- Version Control Generated Code: Committing generated files ensures team-wide consistency and enables code review of contract changes
- Customize Through Configuration: Extend generators via configuration files or wrapper classes, never by editing generated code directly
- Validate Specifications: Invalid specifications produce invalid code; validate schemas before generation to catch errors early
OpenAPI Code Generation
OpenAPI specifications define RESTful API contracts in YAML or JSON. These machine-readable contracts describe endpoints, HTTP methods, request/response structures, validation rules, and error responses. Generating code from OpenAPI specs ensures frontend and backend implement identical contracts - the compiler enforces this consistency.
This diagram illustrates schema-first development flow: a single OpenAPI specification generates both frontend client code and backend server interfaces. Changes to the specification propagate to both sides, causing compilation errors if either side doesn't implement the updated contract.
See OpenAPI Specifications for contract design best practices and API Contracts for contract-first vs code-first trade-offs.
Generating TypeScript API Clients
OpenAPI Generator parses YAML/JSON specifications and produces TypeScript interfaces (types representing request/response DTOs), enums (for status codes, categories), and fetch-based HTTP client methods (one method per API operation). The generated client provides compile-time type safety - if the API contract changes (e.g., a required field is added to PaymentRequest), the TypeScript compiler immediately flags every usage site with a compilation error.
Configuration:
// openapitools.json
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.2.0",
"generators": {
"payment-api": {
"generatorName": "typescript-fetch",
"output": "src/generated/api",
"inputSpec": "../backend/openapi/payment-api.yaml",
"additionalProperties": {
"typescriptThreePlus": true,
"supportsES6": true,
"withInterfaces": true,
"modelPropertyNaming": "camelCase",
"enumPropertyNaming": "UPPERCASE",
"npmName": "@bank/payment-api-client",
"npmVersion": "1.5.0"
}
}
}
}
}
The inputSpec points to the authoritative OpenAPI specification. The typescript-fetch generator produces modern TypeScript using the Fetch API. The modelPropertyNaming: "camelCase" option transforms snake_case API fields to camelCase TypeScript properties, matching JavaScript conventions.
Package.json Scripts:
{
"scripts": {
"generate:api": "openapi-generator-cli generate",
"generate:api:watch": "nodemon --watch ../backend/openapi --exec npm run generate:api",
"prebuild": "npm run generate:api",
"pretest": "npm run generate:api"
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.7.0",
"nodemon": "^3.0.2"
}
}
The prebuild and pretest hooks regenerate clients before builds and tests, ensuring generated code stays synchronized with the specification. The watch script enables real-time regeneration during development - specification changes immediately update generated code.
Generated Structure:
src/generated/api/
├── apis/
│ └── PaymentsApi.ts # API client class with typed methods
├── models/
│ ├── PaymentRequest.ts # Request DTO interface
│ ├── PaymentResponse.ts # Response DTO interface
│ ├── PaymentStatus.ts # Enum for payment states
│ └── index.ts
├── runtime.ts # Base runtime (fetch wrapper, error handling)
└── index.ts # Barrel export
The apis/ directory contains client classes with methods like createPayment(). The models/ directory contains TypeScript interfaces representing API data structures. The runtime.ts file provides shared HTTP logic (authentication, error handling, request/response transformations).
Generating Spring Boot Server Stubs
While Springdoc enables code-first OpenAPI generation, contract-first development generates Spring Boot controller interfaces from OpenAPI specifications. The generator creates controller interfaces annotated with Spring's @RestController, @GetMapping, @PostMapping, etc. You implement these interfaces in concrete classes. This ensures implementation matches the contract exactly - if your implementation signature doesn't match the generated interface, the Java compiler reports an error.
Gradle Configuration:
// build.gradle
plugins {
id "org.openapi.generator" version "7.2.0"
}
openApiGenerate {
generatorName = "spring"
inputSpec = "$rootDir/src/main/resources/openapi/payment-api.yaml"
outputDir = "$buildDir/generated"
apiPackage = "com.bank.payments.api"
modelPackage = "com.bank.payments.model"
configOptions = [
interfaceOnly: "true",
useSpringBoot3: "true",
useTags: "true",
delegatePattern: "true",
performBeanValidation: "true",
useBeanValidation: "true"
]
}
sourceSets {
main {
java {
srcDir "$buildDir/generated/src/main/java"
}
}
}
compileJava.dependsOn tasks.openApiGenerate
The interfaceOnly: "true" option generates only interfaces, not implementations - you write the implementation logic. useBeanValidation: "true" adds @Valid, @NotNull, etc., based on the OpenAPI schema's validation constraints. compileJava.dependsOn tasks.openApiGenerate ensures code generation runs before compilation.
Implementing Generated Interfaces:
// Generated interface (do not edit)
@Generated("org.openapitools.codegen.languages.SpringCodegen")
@Validated
@Tag(name = "payments", description = "Payment operations")
public interface PaymentsApi {
@Operation(summary = "Create payment")
@ApiResponses(...)
@PostMapping(value = "/payments", produces = "application/json")
ResponseEntity<PaymentResponse> createPayment(
@Valid @RequestBody PaymentRequest request
);
}
// Your implementation
@RestController
@RequestMapping("/api/v1")
public class PaymentController implements PaymentsApi {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Override
public ResponseEntity<PaymentResponse> createPayment(PaymentRequest request) {
PaymentResponse response = paymentService.processPayment(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
The generated PaymentsApi interface declares method signatures with Spring annotations. Your PaymentController implements this interface. If the OpenAPI spec changes (e.g., a new required parameter is added), the interface changes, and your implementation fails to compile until you update it.
See Frontend API Integration for using generated clients with React Query.
Database Migration Generation
Database migrations define incremental, versioned schema changes. Unlike JPA's ddl-auto, migrations provide:
- Version Control: Each migration is a numbered file tracked in Git
- Auditability: Database history is visible in migration files
- Rollback Support: Migrations can define undo operations
- Multi-environment Consistency: The same migrations apply to dev, staging, and production
This diagram shows sequential migration application. Flyway applies V1, records it in flyway_schema_history, then applies V2, etc. If V3 was already applied to production, Flyway skips it and applies only V4 when deploying.
See Database Migrations for comprehensive migration patterns, zero-downtime strategies, and rollback procedures, as well as Database Design for table design and indexing strategies.
Flyway SQL Migrations
Flyway executes versioned SQL scripts sequentially. It tracks applied migrations in a flyway_schema_history table. Migrations are immutable - once applied to production, never modify them. If you need to change a column, create a new migration that adds/modifies the column rather than editing the original migration.
Migration File Structure:
src/main/resources/db/migration/
├── V1__create_payment_tables.sql
├── V2__add_payment_status_index.sql
├── V3__add_audit_columns.sql
└── V4__add_currency_support.sql
Naming Convention: V<version>__<description>.sql
- Version: Integer or dotted (e.g.,
V1,V1_1,V2_0_1) - Flyway sorts numerically - Double underscore
__separates version from description (single underscore in version, double before description) - Description: Snake_case words describing the change
Example Migration:
-- V1__create_payment_tables.sql
CREATE TABLE payments (
transaction_id UUID PRIMARY KEY,
amount DECIMAL(19, 2) NOT NULL,
currency VARCHAR(3) NOT NULL,
recipient VARCHAR(255) NOT NULL,
reference VARCHAR(500),
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP,
CONSTRAINT chk_amount_positive CHECK (amount > 0),
CONSTRAINT chk_currency_valid CHECK (currency IN ('USD', 'EUR', 'GBP', 'JPY', 'CAD'))
);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at DESC);
-- Audit table for payment changes
CREATE TABLE payment_audit (
id BIGSERIAL PRIMARY KEY,
transaction_id UUID NOT NULL,
changed_by VARCHAR(255) NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
old_status VARCHAR(20),
new_status VARCHAR(20),
change_reason TEXT,
FOREIGN KEY (transaction_id) REFERENCES payments(transaction_id)
);
This migration creates two tables atomically. The CHECK constraints enforce business rules at the database level (amounts must be positive, currencies must be valid). The indexes optimize common queries (filtering by status, sorting by creation date). The audit table tracks payment state changes - critical for financial applications requiring audit trails.
Gradle Configuration:
// build.gradle
plugins {
id 'org.flywaydb.flyway' version '10.4.1'
}
dependencies {
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
}
flyway {
url = 'jdbc:postgresql://localhost:5432/payments_db'
user = 'db_user'
password = System.getenv('DB_PASSWORD')
locations = ['classpath:db/migration']
baselineOnMigrate = true
validateOnMigrate = true
}
The baselineOnMigrate: true setting handles existing databases - Flyway creates a baseline entry in flyway_schema_history for the current schema. validateOnMigrate: true validates checksums to detect modified migrations (preventing accidental changes to applied migrations).
Running Migrations:
# Apply pending migrations
./gradlew flywayMigrate
# Validate migrations (check for modified files)
./gradlew flywayValidate
# View migration status
./gradlew flywayInfo
# Repair checksums (use carefully after fixing migration files)
./gradlew flywayRepair
The flywayInfo command shows applied migrations, pending migrations, and their states. Use flywayRepair only to fix checksum mismatches (e.g., after fixing line endings), never to modify applied migrations.
Spring Boot Integration:
# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
out-of-order: false
clean-disabled: true # Never allow clean in production!
Spring Boot automatically runs Flyway migrations on startup. The clean-disabled: true setting prevents accidental flyway clean commands (which drop all database objects) in production. out-of-order: false enforces sequential migration application - if V5 is pending but V6 is applied, Flyway fails rather than applying V5 out of sequence.
Never modify applied migrations - create new ones instead. Test migrations on staging first (with production-like data volume). Backup the database before applying migrations. Make migrations reversible (write a corresponding down migration). Disable clean in production (it drops all objects). Use transactions where possible (BEGIN; ... COMMIT;), though DDL statements auto-commit in some databases.
Liquibase XML/YAML Migrations
Liquibase uses database-agnostic XML/YAML changesets. Unlike Flyway's SQL (which uses database-specific syntax), Liquibase abstracts database operations. At runtime, Liquibase translates changesets to the target database's SQL dialect. This enables the same migration file to work across PostgreSQL, MySQL, Oracle, and SQL Server. Liquibase tracks changes in DATABASECHANGELOG with checksums to detect modifications. Built-in rollback support contrasts with Flyway's manual rollback scripts.
Changeset Structure:
# src/main/resources/db/changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: db/changelog/changes/v1.0-create-tables.yaml
- include:
file: db/changelog/changes/v1.1-add-indexes.yaml
- include:
file: db/changelog/changes/v1.2-add-audit-columns.yaml
The master changelog includes individual change files. This modular structure organizes migrations by feature or version. Liquibase applies changes from all included files sequentially.
Example Changeset:
# src/main/resources/db/changelog/changes/v1.0-create-tables.yaml
databaseChangeLog:
- changeSet:
id: 1
author: john.doe
comment: Create payments table
changes:
- createTable:
tableName: payments
columns:
- column:
name: transaction_id
type: UUID
constraints:
primaryKey: true
nullable: false
- column:
name: amount
type: DECIMAL(19, 2)
constraints:
nullable: false
- column:
name: currency
type: VARCHAR(3)
constraints:
nullable: false
- column:
name: status
type: VARCHAR(20)
constraints:
nullable: false
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
constraints:
nullable: false
rollback:
- dropTable:
tableName: payments
- changeSet:
id: 2
author: john.doe
comment: Add indexes on payments table
changes:
- createIndex:
indexName: idx_payments_status
tableName: payments
columns:
- column:
name: status
- createIndex:
indexName: idx_payments_created_at
tableName: payments
columns:
- column:
name: created_at
descending: true
rollback:
- dropIndex:
indexName: idx_payments_status
tableName: payments
- dropIndex:
indexName: idx_payments_created_at
tableName: payments
Each changeSet has a unique id (within the file) and author. The rollback section defines how to undo the change. Liquibase automatically generates rollback SQL for simple operations (like createTable), but explicit rollback definitions handle complex scenarios.
Gradle Configuration:
// build.gradle
plugins {
id 'org.liquibase.gradle' version '2.2.0'
}
dependencies {
implementation 'org.liquibase:liquibase-core:4.25.0'
liquibaseRuntime 'org.liquibase:liquibase-core:4.25.0'
liquibaseRuntime 'org.postgresql:postgresql'
liquibaseRuntime 'info.picocli:picocli:4.7.5'
}
liquibase {
activities {
main {
changelogFile 'src/main/resources/db/changelog/db.changelog-master.yaml'
url 'jdbc:postgresql://localhost:5432/payments_db'
username 'db_user'
password System.getenv('DB_PASSWORD')
}
}
}
Running Migrations:
# Apply pending changesets
./gradlew update
# Generate SQL preview (don't apply)
./gradlew updateSQL
# Rollback last changeset
./gradlew rollbackCount -PliquibaseCommandValue=1
# Generate diff between database and changelog
./gradlew diff
The updateSQL command generates SQL without executing it - use this to review changes before applying. The diff command compares the current database schema to the changelog, identifying drift (manual schema changes bypassing migrations).
Liquibase vs Flyway:
| Feature | Flyway | Liquibase |
|---|---|---|
| Migration Format | SQL (native dialect) | XML/YAML (database-agnostic) |
| Rollback | Manual SQL scripts | Built-in rollback support |
| Database Support | Most SQL databases | Most SQL databases + NoSQL |
| Learning Curve | Simple (just SQL) | Steeper (XML/YAML syntax) |
| Diff Generation | No | Yes (can diff database vs changelog) |
| Conditional Logic | Via SQL | Via preconditions |
| Best For | Single-database projects (e.g., PostgreSQL-only) | Multi-database projects requiring portability |
Choose Flyway for PostgreSQL-only projects where SQL readability is prioritized. Choose Liquibase for multi-database environments or when automatic rollback generation is critical.
JPA Schema Generation (Development Only)
JPA generates database schema from entity annotations. Hibernate inspects @Entity classes and creates tables, columns, indexes, and foreign keys. This accelerates local development - no manual migration writing. Never use in production - use JPA-generated DDL as a starting point for Flyway/Liquibase migrations.
# application-dev.yml (development only!)
spring:
jpa:
hibernate:
ddl-auto: update # Creates/updates tables based on entities
properties:
hibernate:
format_sql: true
show_sql: true
# Generate DDL files
properties:
javax.persistence.schema-generation:
scripts:
action: create
create-target: target/generated-ddl.sql
The ddl-auto: update setting creates missing tables/columns and updates existing ones. The create-target setting writes generated DDL to a file for review.
Generate DDL from Entities:
@Entity
@Table(name = "payments")
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "transaction_id")
private UUID transactionId;
@Column(nullable = false, precision = 19, scale = 2)
private BigDecimal amount;
@Column(nullable = false, length = 3)
private String currency;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentStatus status;
@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;
// Getters, setters, equals, hashCode
}
JPA reads annotations and generates: CREATE TABLE payments (transaction_id UUID PRIMARY KEY, amount DECIMAL(19,2) NOT NULL, ...). See Spring Boot Data for JPA entity design patterns.
Workflow for Production Migrations:
- Use
ddl-auto: updatein local development - Let JPA generate DDL to
target/generated-ddl.sql - Review generated SQL (add indexes, constraints, custom types)
- Create proper Flyway migration from generated SQL
- Test migration on local database
- Commit migration to version control
- Apply to staging/production via Flyway
JPA schema generation bypasses version control (changes are untracked), is non-deterministic (depends on entity scanning order), provides no rollback mechanism, risks data loss (create-drop destroys data on shutdown), and bypasses code review. Use Flyway/Liquibase for all environments except local development.
GraphQL Code Generation
GraphQL schemas define type-safe query APIs. Unlike REST's endpoint-based contracts, GraphQL uses a single endpoint with a typed schema. Generating code from GraphQL schemas ensures type safety between client queries and server resolvers - the compiler enforces that queries only request fields defined in the schema.
This diagram shows schema-first GraphQL development. A single schema generates TypeScript types for frontend queries and Java types for backend resolvers, ensuring both sides agree on the data structure.
See GraphQL API Design for schema design, resolver optimization, and performance monitoring.
GraphQL Code Generation for TypeScript
GraphQL Code Generator parses schemas and queries to generate TypeScript interfaces, operation types, and framework-specific hooks (Apollo, urql). Generated hooks provide compile-time type safety - if you add a field to a query (createdBy), but the schema doesn't define it, TypeScript compilation fails immediately.
Installation:
npm install --save-dev @graphql-codegen/cli
npm install --save-dev @graphql-codegen/typescript
npm install --save-dev @graphql-codegen/typescript-operations
npm install --save-dev @graphql-codegen/typescript-react-apollo
Configuration:
# codegen.yml
overwrite: true
schema: "http://localhost:8080/graphql" # Schema source (introspection)
documents: "src/**/*.graphql" # Query/mutation files
generates:
src/generated/graphql.ts:
plugins:
- typescript # Generate base types
- typescript-operations # Generate operation types
- typescript-react-apollo # Generate React hooks
config:
withHooks: true
withComponent: false
withHOC: false
scalars:
UUID: string
DateTime: string
Decimal: number
The schema can be a URL (introspection), a local .graphql file, or a glob pattern. The documents glob finds all .graphql files containing queries/mutations. The scalars mapping defines TypeScript types for custom scalars (GraphQL doesn't know UUID maps to JavaScript string).
GraphQL Schema Example:
# schema.graphql
type Payment {
transactionId: UUID!
amount: Decimal!
currency: Currency!
recipient: String!
reference: String
status: PaymentStatus!
createdAt: DateTime!
updatedAt: DateTime!
}
enum Currency {
USD
EUR
GBP
JPY
CAD
}
enum PaymentStatus {
PENDING
PROCESSING
COMPLETED
FAILED
CANCELLED
}
input CreatePaymentInput {
amount: Decimal!
currency: Currency!
recipient: String!
reference: String
}
type Query {
payment(transactionId: UUID!): Payment
payments(status: PaymentStatus, limit: Int = 20, offset: Int = 0): [Payment!]!
}
type Mutation {
createPayment(input: CreatePaymentInput!): Payment!
cancelPayment(transactionId: UUID!): Payment!
}
This schema defines types (Payment), enums (Currency, PaymentStatus), inputs (CreatePaymentInput), queries (payment, payments), and mutations (createPayment, cancelPayment). The ! denotes non-null fields.
GraphQL Query Files:
# src/queries/payments.graphql
query GetPayment($transactionId: UUID!) {
payment(transactionId: $transactionId) {
transactionId
amount
currency
recipient
reference
status
createdAt
}
}
mutation CreatePayment($input: CreatePaymentInput!) {
createPayment(input: $input) {
transactionId
amount
currency
status
createdAt
}
}
Each query/mutation file defines operations. The generator parses these to create typed hooks - useGetPaymentQuery, useCreatePaymentMutation.
Generated TypeScript Types:
// src/generated/graphql.ts (generated, do not edit)
export type Maybe<T> = T | null;
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
UUID: string;
DateTime: string;
Decimal: number;
};
export enum Currency {
Usd = 'USD',
Eur = 'EUR',
Gbp = 'GBP',
Jpy = 'JPY',
Cad = 'CAD'
}
export enum PaymentStatus {
Pending = 'PENDING',
Processing = 'PROCESSING',
Completed = 'COMPLETED',
Failed = 'FAILED',
Cancelled = 'CANCELLED'
}
export type Payment = {
__typename?: 'Payment';
transactionId: Scalars['UUID'];
amount: Scalars['Decimal'];
currency: Currency;
recipient: Scalars['String'];
reference?: Maybe<Scalars['String']>;
status: PaymentStatus;
createdAt: Scalars['DateTime'];
};
// Generated hooks
export function useGetPaymentQuery(
baseOptions: Apollo.QueryHookOptions<GetPaymentQuery, GetPaymentQueryVariables>
) {
return Apollo.useQuery<GetPaymentQuery, GetPaymentQueryVariables>(GetPaymentDocument, baseOptions);
}
export function useCreatePaymentMutation(
baseOptions?: Apollo.MutationHookOptions<CreatePaymentMutation, CreatePaymentMutationVariables>
) {
return Apollo.useMutation<CreatePaymentMutation, CreatePaymentMutationVariables>(CreatePaymentDocument, baseOptions);
}
The generator produces enums (Currency, PaymentStatus), types (Payment), and hooks (useGetPaymentQuery). The hooks are fully typed - TypeScript infers variable types, response shapes, and error types.
Using Generated Types:
// src/components/PaymentForm.tsx
import { useCreatePaymentMutation, Currency } from '../generated/graphql';
export function PaymentForm() {
const [createPayment, { data, loading, error }] = useCreatePaymentMutation();
const handleSubmit = async (formData: any) => {
// Generated types ensure type safety
const result = await createPayment({
variables: {
input: {
amount: formData.amount,
currency: Currency.Usd, // Enum from generated types
recipient: formData.recipient,
reference: formData.reference
}
}
});
// result.data.createPayment is fully typed
console.log('Payment created:', result.data?.createPayment?.transactionId);
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
The useCreatePaymentMutation hook returns typed data, loading, error. The variables object is type-checked - if you pass an invalid currency or omit a required field, TypeScript compilation fails.
Scripts:
{
"scripts": {
"generate:graphql": "graphql-codegen --config codegen.yml",
"generate:graphql:watch": "graphql-codegen --config codegen.yml --watch",
"prebuild": "npm run generate:graphql"
}
}
GraphQL Code Generation for Java
Generating Java resolvers and types from GraphQL schemas ensures backend type safety. DGS Codegen (Netflix) generates Java classes from GraphQL schemas, including types, inputs, enums, and resolver scaffolding.
Gradle Configuration (DGS Codegen):
// build.gradle
plugins {
id 'com.netflix.dgs.codegen' version '6.0.3'
}
dependencies {
implementation 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:latest.release'
}
generateJava {
schemaPaths = ["${projectDir}/src/main/resources/schema"]
packageName = 'com.bank.payments.graphql.generated'
generateClient = true
generateDataTypes = true
typeMapping = [
'UUID': 'java.util.UUID',
'DateTime': 'java.time.Instant',
'Decimal': 'java.math.BigDecimal'
]
}
The typeMapping defines Java types for custom GraphQL scalars. The generateDataTypes: true option creates POJOs for GraphQL types.
Generated Java Types:
// Generated by DGS Codegen (do not edit)
package com.bank.payments.graphql.generated.types;
public class Payment {
private UUID transactionId;
private BigDecimal amount;
private Currency currency;
private String recipient;
private String reference;
private PaymentStatus status;
private Instant createdAt;
// Getters, setters, builder
}
public enum Currency {
USD, EUR, GBP, JPY, CAD
}
public enum PaymentStatus {
PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED
}
public class CreatePaymentInput {
private BigDecimal amount;
private Currency currency;
private String recipient;
private String reference;
// Getters, setters, builder
}
Implementing Resolvers:
// Your implementation uses generated types
@DgsComponent
public class PaymentDataFetcher {
private final PaymentService paymentService;
@DgsQuery
public Payment payment(@InputArgument UUID transactionId) {
return paymentService.getPayment(transactionId)
.map(this::toGraphQLPayment)
.orElseThrow(() -> new PaymentNotFoundException(transactionId));
}
@DgsMutation
public Payment createPayment(@InputArgument CreatePaymentInput input) {
// Input types are generated and type-safe
PaymentRequest request = PaymentRequest.builder()
.amount(input.getAmount())
.currency(input.getCurrency())
.recipient(input.getRecipient())
.reference(input.getReference())
.build();
PaymentResponse response = paymentService.processPayment(request);
return toGraphQLPayment(response);
}
private Payment toGraphQLPayment(PaymentResponse response) {
return Payment.builder()
.transactionId(response.getTransactionId())
.amount(response.getAmount())
.currency(Currency.valueOf(response.getCurrency()))
.status(PaymentStatus.valueOf(response.getStatus().name()))
.createdAt(response.getCreatedAt())
.build();
}
}
The @DgsQuery and @DgsMutation annotations define resolvers. The @InputArgument annotation binds method parameters to GraphQL arguments. The generated CreatePaymentInput type ensures type safety - if the schema changes, the generated class changes, causing compilation errors in dependent code.
IDE Templates and Snippets
IDE templates expand abbreviations into common code patterns. Instead of typing boilerplate repeatedly, type an abbreviation (e.g., sbrc), press Tab, and the IDE expands it to a full REST controller. Templates reduce typing, enforce consistency (all controllers follow the same structure), and minimize typos. See IDE Setup for comprehensive template configuration and productivity features.
IntelliJ IDEA Live Templates
Live Templates expand abbreviations when you press Tab. They support variables (placeholders you fill in), expressions (dynamic values like filenames), and cursor positioning (where the cursor lands after expansion).
Accessing Live Templates: Settings > Editor > Live Templates
Creating Custom Templates:
<!-- .idea/templates/Java.xml -->
<templateSet group="Java">
<!-- Spring Boot REST Controller -->
<template name="sbrc" value="@RestController @RequestMapping("/api/v1/$PATH$") @RequiredArgsConstructor @Validated public class $NAME$ { private final $SERVICE$ $SERVICE_VAR$; $END$ } " description="Spring Boot REST Controller" toReformat="true" toShortenFQNames="true">
<variable name="PATH" expression="" defaultValue=""payments"" alwaysStopAt="true" />
<variable name="NAME" expression="" defaultValue=""PaymentController"" alwaysStopAt="true" />
<variable name="SERVICE" expression="" defaultValue=""PaymentService"" alwaysStopAt="true" />
<variable name="SERVICE_VAR" expression="decapitalize(SERVICE)" defaultValue="" alwaysStopAt="false" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
<!-- JUnit 5 Test Class -->
<template name="jtest" value="@ExtendWith(MockitoExtension.class) class $NAME$ { @Mock private $DEPENDENCY$ $DEPENDENCY_VAR$; @InjectMocks private $CLASS_UNDER_TEST$ $CLASS_VAR$; @Test void $METHOD$() { $END$ } } " description="JUnit 5 Test Class" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="fileNameWithoutExtension() + "Test"" defaultValue="" alwaysStopAt="false" />
<variable name="DEPENDENCY" expression="" defaultValue=""Repository"" alwaysStopAt="true" />
<variable name="DEPENDENCY_VAR" expression="decapitalize(DEPENDENCY)" defaultValue="" alwaysStopAt="false" />
<variable name="CLASS_UNDER_TEST" expression="" defaultValue=""Service"" alwaysStopAt="true" />
<variable name="CLASS_VAR" expression="decapitalize(CLASS_UNDER_TEST)" defaultValue="" alwaysStopAt="false" />
<variable name="METHOD" expression="" defaultValue=""shouldProcessPaymentSuccessfully"" alwaysStopAt="true" />
<context>
<option name="JAVA_DECLARATION" value="true" />
</context>
</template>
</templateSet>
The $PATH$, $NAME$, $SERVICE$ are variables (placeholders). The decapitalize(SERVICE) expression transforms PaymentService to paymentService. The $END$ marker places the cursor after expansion.
Using Live Templates:
- Type abbreviation (e.g.,
sbrc) - Press Tab
- Template expands with placeholders highlighted
- Fill in placeholders, press Tab to move between them
- Press Enter when done
Common Spring Boot Templates:
| Abbreviation | Expands To | Description |
|---|---|---|
sbrc | REST Controller class | Spring Boot REST controller with annotations |
sbrm | @GetMapping method | GET endpoint method |
sbrp | @PostMapping method | POST endpoint method |
jtest | JUnit test class | Test class with Mockito annotations |
jmock | @Mock field | Mock dependency field |
jverify | verify(mock).method() | Mockito verification |
jassert | assertThat(...).isEqualTo(...) | AssertJ assertion |
VS Code Snippets
VS Code snippets work similarly to Live Templates but use JSON format. Snippets support placeholders (${1:name}), choices (${1|option1,option2|}), and dynamic values ($TM_FILENAME).
Creating Snippets: File > Preferences > Configure User Snippets
TypeScript React Snippets:
// typescript.json
{
"React Functional Component": {
"prefix": "rfc",
"body": [
"import React from 'react';",
"",
"interface ${1:ComponentName}Props {",
" ${2:prop}: ${3:string};",
"}",
"",
"export function ${1:ComponentName}({ ${2:prop} }: ${1:ComponentName}Props) {",
" return (",
" <div>",
" $0",
" </div>",
" );",
"}",
""
],
"description": "React functional component with TypeScript"
},
"React Query Hook": {
"prefix": "usequery",
"body": [
"export function use${1:ResourceName}(${2:id}: string) {",
" return useQuery({",
" queryKey: ['${3:resource}', ${2:id}],",
" queryFn: () => ${4:apiClient}.get${1:ResourceName}(${2:id}),",
" enabled: !!${2:id}",
" });",
"}",
"$0"
],
"description": "React Query hook"
},
"React Mutation Hook": {
"prefix": "usemutation",
"body": [
"export function use${1:Action}${2:ResourceName}() {",
" const queryClient = useQueryClient();",
" ",
" return useMutation({",
" mutationFn: (${3:data}: ${4:InputType}) => ${5:apiClient}.${6:action}${2:ResourceName}(${3:data}),",
" onSuccess: () => {",
" queryClient.invalidateQueries({ queryKey: ['${7:resource}'] });",
" }",
" });",
"}",
"$0"
],
"description": "React Query mutation hook"
}
}
The ${1:ComponentName} is a placeholder with default value ComponentName. The $0 is the final cursor position.
Using VS Code Snippets:
- Type prefix (e.g.,
rfc) - Select snippet from autocomplete
- Fill in placeholders (Tab to move between them)
- Template expands to full code
Sharing Snippets Across Team:
// .vscode/typescript.code-snippets (committed to repo)
{
"Payment Form Component": {
"prefix": "payment-form",
"scope": "typescript,typescriptreact",
"body": [
"import { useForm } from 'react-hook-form';",
"import { useCreatePayment } from '@/hooks/usePayments';",
"import { PaymentRequest } from '@/generated/api';",
"",
"export function PaymentForm() {",
" const { register, handleSubmit, formState: { errors } } = useForm<PaymentRequest>();",
" const createPayment = useCreatePayment();",
" ",
" const onSubmit = async (data: PaymentRequest) => {",
" await createPayment.mutateAsync(data);",
" };",
" ",
" return (",
" <form onSubmit={handleSubmit(onSubmit)}>",
" $0",
" </form>",
" );",
"}"
],
"description": "Payment form component with validation"
}
}
Committing .vscode/ snippets to the repo shares them team-wide. New team members automatically get project-specific snippets when they clone the repository.
Project Scaffolding Tools
Scaffolding tools generate complete project structures with dependencies, configuration, and boilerplate already in place. This eliminates manual setup (creating directories, configuring Gradle, adding dependencies) and ensures projects start with best practices.
Spring Initializr
Spring Initializr generates Spring Boot projects with selected dependencies, Gradle/Maven configuration, and package structure.
Using Web Interface:
- Navigate to start.spring.io
- Select options:
- Project: Gradle - Groovy
- Language: Java
- Spring Boot: 3.2.x (latest stable)
- Project Metadata: Group (
com.bank), Artifact (payment-service), Name, Package - Packaging: Jar
- Java: 21
- Add dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver, Validation, Flyway, Springdoc OpenAPI, Actuator
- Click "Generate" to download ZIP
Using Command Line:
# Using curl
curl https://start.spring.io/starter.zip \
-d dependencies=web,data-jpa,postgresql,validation,flyway,actuator,docker-compose \
-d bootVersion=3.4.1 \
-d javaVersion=25 \
-d type=gradle-project \
-d groupId=com.bank \
-d artifactId=payment-service \
-d name=payment-service \
-d packageName=com.bank.payments \
-o payment-service.zip
unzip payment-service.zip
cd payment-service
The -d dependencies=... flag specifies Spring Boot starters. The -d type=gradle-project flag selects Gradle over Maven.
Using Spring CLI:
# Install Spring CLI
sdk install springboot
# Generate project
spring init \
--dependencies=web,data-jpa,postgresql,validation,flyway \
--build=gradle \
--java-version=21 \
--group-id=com.bank \
--artifact-id=payment-service \
--name=payment-service \
--package-name=com.bank.payments \
payment-service
Customizing Generated Project:
After generation, customize:
- Update
application.ymlwith environment-specific configuration (database URL, pool size, Flyway settings) - Configure Gradle plugins (Spotless for formatting, JaCoCo for coverage, PITest for mutation testing, Docker for containerization)
- Add OpenAPI specification to
src/main/resources/openapi/ - Create database migrations in
src/main/resources/db/migration/ - Set up CI/CD pipeline (
.gitlab-ci.ymlfor GitLab) - see Pipeline Configuration - Add README with project documentation
Create React App / Vite
Vite is the recommended tool for React projects. Vite provides faster development server startup (ES modules instead of bundling), faster hot module replacement (HMR), and smaller production bundles than Create React App (CRA). CRA is in maintenance mode; new projects should use Vite.
Creating React + TypeScript Project:
# Using Vite (recommended)
npm create vite@latest payment-app -- --template react-ts
cd payment-app
npm install
# Add common dependencies
npm install @tanstack/react-query @tanstack/react-query-devtools
npm install react-router-dom
npm install zustand
npm install react-hook-form
npm install axios
# Add dev dependencies
npm install --save-dev @types/node
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install --save-dev prettier eslint-config-prettier
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install --save-dev vitest @vitest/ui
The --template react-ts flag generates a TypeScript React project. React Query provides data fetching, Zustand manages global state, React Hook Form handles forms, and Vitest runs tests.
Project Structure After Setup:
payment-app/
├── src/
│ ├── components/ # React components
│ ├── hooks/ # Custom hooks (React Query, Zustand)
│ ├── services/ # API services
│ ├── generated/ # Generated API clients (from OpenAPI)
│ ├── store/ # Zustand stores
│ ├── types/ # TypeScript types
│ ├── utils/ # Utility functions
│ ├── App.tsx
│ └── main.tsx
├── public/
├── tests/
├── .eslintrc.json
├── .prettierrc
├── tsconfig.json
├── vite.config.ts
└── package.json
Angular CLI
Angular CLI scaffolds Angular applications with routing, modules, components, services, and testing infrastructure.
# Install Angular CLI
npm install -g @angular/cli@17
# Create new Angular project
ng new payment-app \
--routing \
--style=scss \
--strict
cd payment-app
# Generate module with routing
ng generate module features/payments --routing
# Generate component
ng generate component features/payments/components/payment-form
# Generate service
ng generate service features/payments/services/payment
# Generate interface
ng generate interface features/payments/models/payment
# Generate guard
ng generate guard core/guards/auth
The --routing flag adds Angular Router. The --strict flag enables strict TypeScript checking. The CLI generates TypeScript files, HTML templates, SCSS stylesheets, and Jest specs.
Customization:
// angular.json - Configure linting, testing, building
{
"projects": {
"payment-app": {
"architect": {
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
}
}
}
}
React Native CLI
React Native CLI scaffolds mobile applications for iOS and Android.
# Create React Native project
npx react-native@latest init PaymentApp --template react-native-template-typescript
cd PaymentApp
# Install common dependencies
npm install @react-navigation/native @react-navigation/stack
npm install react-native-screens react-native-safe-area-context
npm install @tanstack/react-query
npm install zustand
npm install axios
# iOS specific
cd ios && pod install && cd ..
# Run on iOS
npm run ios
# Run on Android
npm run android
The --template react-native-template-typescript flag generates a TypeScript project. React Navigation provides navigation, React Query handles data fetching, and Zustand manages state.
Anti-Patterns
Editing Generated Code Directly
Bad:
// src/generated/api/PaymentsApi.ts (generated)
export class PaymentsApi {
// Manually added method (will be lost on regeneration!)
async retryPayment(paymentId: string) {
// custom logic
}
}
Manual edits to generated files are lost when regeneration occurs. Generated files are marked with @generated comments warning against manual edits.
Good:
// src/services/PaymentApiWrapper.ts
import { PaymentsApi, Configuration } from '../generated/api';
export class PaymentApiWrapper extends PaymentsApi {
constructor(config: Configuration) {
super(config);
}
// Add custom methods here
async retryPayment(paymentId: string) {
// Call generated method with retry logic
return this.createPayment({ paymentRequest: { /* retry data */ } });
}
// Override generated method if needed
async createPayment(params: any) {
console.log('Creating payment:', params);
return super.createPayment(params);
}
}
Wrapper classes extend generated code without modifying it. Regeneration doesn't affect custom logic.
Ignoring Generated Files in Version Control
Bad:
# .gitignore
src/generated/ # Don't commit generated code
Ignoring generated files forces every developer to regenerate code on checkout. Differences in generator versions, configurations, or timing cause inconsistent generated code across the team.
Good:
Commit generated code to version control. This ensures:
- Everyone uses identical generated code
- Code reviews show API contract changes
- No build-time generation delays
- Deterministic builds
Alternative for Large Teams: Publish generated code as npm packages:
// package.json for generated client
{
"name": "@bank/payment-api-client",
"version": "1.5.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"generate": "openapi-generator-cli generate",
"build": "npm run generate && tsc",
"prepublishOnly": "npm run build"
}
}
Consumer projects install the package (npm install @bank/payment-api-client), avoiding generation entirely.
Modifying Applied Migrations
Bad:
-- V1__create_payment_tables.sql (already applied to production)
-- Developer edits file to add new column
CREATE TABLE payments (
transaction_id UUID PRIMARY KEY,
amount DECIMAL(19, 2) NOT NULL,
new_column VARCHAR(255) -- Added after production deployment
);
Flyway checksums detect modifications to applied migrations and fails. Even if you use flywayRepair to fix checksums, production databases won't receive the change (migration already applied).
Good:
-- V2__add_new_column_to_payments.sql (new migration)
ALTER TABLE payments ADD COLUMN new_column VARCHAR(255);
Create new migrations for changes. This maintains migration history and applies changes to all environments.
Using JPA Schema Generation in Production
Bad:
# application-prod.yml (production)
spring:
jpa:
hibernate:
ddl-auto: update # Dangerous in production!
JPA schema generation bypasses version control, code review, and rollback mechanisms. Schema changes are untracked. Data loss occurs with create-drop.
Good:
# application-prod.yml
spring:
jpa:
hibernate:
ddl-auto: validate # Only validates schema matches entities
flyway:
enabled: true
Use Flyway/Liquibase for production. Use JPA's validate mode to ensure entities match the schema (migration-created).
Not Formatting Generated Code
Bad:
Generated code uses different formatting (tabs vs spaces, line length) than team standards, causing linter failures and cluttered diffs.
Good:
{
"scripts": {
"generate:api": "openapi-generator-cli generate && npm run format:generated",
"format:generated": "prettier --write 'src/generated/**/*.ts'"
}
}
Post-generation formatting ensures consistency. Alternatively, configure ESLint to ignore generated files:
// .eslintrc.json
{
"ignorePatterns": ["src/generated/**/*.ts"]
}
Best Practices
Validate Specifications Before Generation
Invalid specifications produce invalid code. Validate OpenAPI specs with openapi-generator-cli validate or swagger-cli validate. Validate GraphQL schemas with graphql-schema-linter.
{
"scripts": {
"validate:openapi": "swagger-cli validate ../backend/openapi/payment-api.yaml",
"pregenerate:api": "npm run validate:openapi"
}
}
Automate Generation in CI/CD
CI pipelines should regenerate code and compare with committed versions. If generated code differs, fail the build and instruct developers to regenerate locally.
# .gitlab-ci.yml
generate-api-clients:
stage: build
script:
- npm run generate:api
# Fail if generated code differs from committed version
- git diff --exit-code src/generated/ || (echo "Generated code changed! Run npm run generate:api locally." && exit 1)
This prevents committing outdated generated code.
Version Specifications with Code
OpenAPI specs, GraphQL schemas, and database migrations are code. Version them in Git alongside implementation code. Tag specification versions matching API versions.
git tag v1.5.0-api
git push --tags
Document Generation Process
README files should explain:
- What gets generated
- How to regenerate code (
npm run generate:api) - Where specifications live (
../backend/openapi/) - What to do when generation fails (validate spec, check generator version)
# README.md
## Code Generation
This project uses OpenAPI Generator to generate TypeScript API clients.
### Regenerating Clients
```bash
npm run generate:api
Updating API Contract
- Update
../backend/openapi/payment-api.yaml - Run
npm run generate:api - Fix compilation errors in code using the API client
- Commit both the spec and generated code
### Use Generator Configuration, Not Manual Edits
Generators support extensive configuration. Use configuration files to customize output rather than editing generated code.
```json
// openapitools.json
{
"generator-cli": {
"generators": {
"payment-api": {
"additionalProperties": {
"typescriptThreePlus": true,
"withInterfaces": true,
"modelPropertyNaming": "camelCase"
},
"globalProperties": {
"modelDocs": "false",
"apiDocs": "false"
}
}
}
}
}
Further Reading
Internal Documentation
- OpenAPI Specifications - Creating and validating OpenAPI specs
- Frontend API Integration - Using generated TypeScript clients with React Query
- Backend API Integration - Spring Boot OpenAPI integration with Springdoc
- Database Design - Table design, normalization, and indexing
- Database Migrations - Zero-downtime migrations and rollback strategies
- Pipeline Configuration - CI/CD automation with code generation
- IDE Setup - IDE templates, live templates, and productivity features
- GraphQL API Design - Schema design, resolver optimization, performance monitoring
External Resources
- OpenAPI Generator Documentation - Comprehensive guide to generators, configuration options, and templates
- OpenAPI Specification - Official OpenAPI 3.0+ specification reference
- Flyway Documentation - Database migration tool with versioning and validation
- Liquibase Documentation - Database schema management with rollback support
- GraphQL Code Generator - TypeScript, React, Apollo, and GraphQL type generation
- Spring Initializr - Spring Boot project generator with dependency management
- Vite Guide - Modern frontend build tool with fast HMR and optimized bundling
- DGS Framework - Netflix's GraphQL framework for Spring Boot with code generation
Related Guidelines
- Spring Boot API Design - REST controller patterns and request validation
- Spring Boot Data Access - JPA entity design and repository patterns
- TypeScript Type Safety - Leveraging generated types for compile-time safety
- React Testing - Mocking generated API clients in tests
- Database Code Review - Reviewing generated migrations and schema changes