OpenAPI

Spark can automatically generate OpenAPI 3.0 specifications from your endpoints. Use annotations to document your API and export a complete spec file.

Usage

Generate an OpenAPI specification with the CLI:

spark openapi

Options:

  • `--output, -o <path>` - Output file path (default: `openapi.json`)

Global Configuration

Configure API metadata using the `@OpenApi` annotation on your main function:

@OpenApi(
  title: 'My API',
  version: '1.0.0',
  description: 'API description',
  servers: ['https://api.example.com'],
  securitySchemes: {
    'BearerAuth': SecurityScheme.http(
      scheme: 'bearer',
      bearerFormat: 'JWT',
    ),
  },
)
void main() {
  // ...
}

Endpoint Documentation

Document individual endpoints using the `@Endpoint` annotation:

@Endpoint(
  path: '/api/users/{id}',
  method: 'GET',
  summary: 'Get user by ID',
  description: 'Retrieves a user by their unique identifier.',
  tags: ['Users'],
  statusCode: 200,  // Happy path status code
  contentTypes: ['application/json'],  // Supported content types
)
class GetUserEndpoint extends SparkEndpoint {
  @override
  Future<UserDto> handler(SparkRequest request) async {
    // Response type (UserDto) is automatically deduced from return type
    return UserDto(name: 'John Doe');
  }
}

The `statusCode` field specifies the HTTP status code for successful responses (defaults to 200). The `contentTypes` field specifies supported content types (defaults to `application/json`).

Automatic Response Deduction

Spark automatically deduces the response type from your handler's return type. You don't need to manually define responses in the annotation.

// Return type is automatically used for OpenAPI response schema
class GetUserEndpoint extends SparkEndpoint {
  @override
  Future<UserDto> handler(SparkRequest request) async {
    return UserDto(name: 'John Doe');  // UserDto schema generated automatically
  }
}

// Works with primitive types too
class HelloEndpoint extends SparkEndpoint {
  @override
  Future<String> handler(SparkRequest request) async {
    return 'Hello World';  // String type in OpenAPI spec
  }
}

Supported return types include `String`, `int`, `double`, `bool`, `List<T>`, `Map<String, dynamic>`, and custom classes with public fields.

Automatic Error Responses

Spark automatically adds error responses to the OpenAPI specification when you throw an `ApiError` (or a subclass) in your endpoints or middleware.

For example, if you throw `ApiError(404, "Not found")`, Spark will add a 404 response to the spec. This means you don't need to manually define standard error responses in the annotation.

Automatic Request Body Deduction

Request bodies are automatically deduced when your endpoint extends `SparkEndpointWithBody<T>`. The type parameter `T` is used to generate the request body schema.

@Endpoint(
  path: '/api/users',
  method: 'POST',
  summary: 'Create user',
  tags: ['Users'],
  statusCode: 201,
  contentTypes: ['application/json'],
)
class CreateUserEndpoint extends SparkEndpointWithBody<CreateUserDto> {
  @override
  Future<UserDto> handler(SparkRequest request, CreateUserDto body) async {
    // CreateUserDto is automatically documented as the request body
    // UserDto is automatically documented as the response
    return UserDto(name: body.name);
  }
}

You can also use `Map<String, dynamic>` as the body type for dynamic JSON payloads:

@Endpoint(path: '/api/details', method: 'POST')
class EchoDetailsEndpoint extends SparkEndpointWithBody<Map<String, dynamic>> {
  @override
  Future<Map<String, dynamic>> handler(
    SparkRequest request,
    Map<String, dynamic> body,
  ) async {
    return body;
  }
}

Parameters

Document query, header, and path parameters using the `parameters` field:

@Endpoint(
  summary: 'List users',
  parameters: [
    Parameter(
      name: 'page',
      inLocation: 'query',
      description: 'Page number',
      required: false,
      schema: {'type': 'integer'},
    ),
    Parameter(
      name: 'limit',
      inLocation: 'query',
      description: 'Items per page',
      schema: {'type': 'integer'},
    ),
    Parameter(
      name: 'Authorization',
      inLocation: 'header',
      description: 'Bearer token',
      required: true,
    ),
  ],
  // ...
)

Valid `inLocation` values: `query`, `header`, `path`, `cookie`. Path parameters (like `{id}`) are automatically detected from the route.

Security Schemes

Define authentication methods in the global configuration:

Bearer Token

securitySchemes: {
  'BearerAuth': SecurityScheme.http(
    scheme: 'bearer',
    bearerFormat: 'JWT',
  ),
}

API Key

securitySchemes: {
  'ApiKey': SecurityScheme.apiKey(
    name: 'X-API-Key',
    inLocation: 'header',  // 'header', 'query', or 'cookie'
  ),
}

OAuth2

securitySchemes: {
  'OAuth2': SecurityScheme.oauth2(
    flows: {
      'authorizationCode': SecuritySchemeFlow(
        authorizationUrl: 'https://auth.example.com/authorize',
        tokenUrl: 'https://auth.example.com/token',
        scopes: {
          'read:users': 'Read user data',
          'write:users': 'Modify user data',
        },
      ),
    },
  ),
}

OpenID Connect

securitySchemes: {
  'OpenID': SecurityScheme.openIdConnect(
    openIdConnectUrl: 'https://auth.example.com/.well-known/openid-configuration',
  ),
}

Apply security to endpoints using the `security` field:

@Endpoint(
  summary: 'Get current user',
  security: [{'BearerAuth': []}],
  // ...
)

Schema Generation

Dart types are automatically mapped to OpenAPI schemas:

  • `String` → `string`
  • `int` → `integer`
  • `double` → `number`
  • `bool` → `boolean`
  • `List<T>` → `array` of T
  • `Map<String, T>` → `object` with additionalProperties of T
  • `DateTime` → `string` with format `date-time`
  • Custom classes → `object` with properties introspected from public fields

Custom classes are automatically introspected via their public fields. No `toJson()` method is required.

// This class will generate an OpenAPI schema automatically
class UserDto {
  final String name;
  final int age;
  final bool isActive;
  
  UserDto({required this.name, required this.age, required this.isActive});
}

Complete Example

// main.dart
@OpenApi(
  title: 'User Service API',
  version: '1.0.0',
  description: 'Manage users and authentication',
  servers: ['https://api.example.com/v1'],
  securitySchemes: {
    'BearerAuth': SecurityScheme.http(
      scheme: 'bearer',
      bearerFormat: 'JWT',
    ),
  },
)
// import 'spark_router.g.dart';

void main() async {
  final server = await createSparkServer(SparkServerConfig(port: 8080));
  print('Running at http://localhost:${server.port}');
}

// DTOs - schemas are generated from public fields
class UserDto {
  final String name;
  UserDto({required this.name});
}

class CreateUserDto {
  final String name;
  final String email;
  CreateUserDto({required this.name, required this.email});
}

// GET endpoint - response type deduced from return type
@Endpoint(
  path: '/api/users/{id}',
  method: 'GET',
  summary: 'Get user by ID',
  description: 'Retrieves a user by their unique identifier.',
  tags: ['Users'],
  security: [{'BearerAuth': []}],
  statusCode: 200,
)
class GetUserEndpoint extends SparkEndpoint {
  @override
  List<Middleware> get middleware => [AuthMiddleware()];

  @override
  Future<UserDto> handler(SparkRequest request) async {
    final id = request.pathParams['id'];
    final user = await userService.findById(id);
    if (user == null) {
      throw NotFoundException('User not found');  // Adds 404 to spec
    }
    return UserDto(name: user.name);  // UserDto schema auto-generated
  }
}

// POST endpoint - request body deduced from SparkEndpointWithBody<T>
@Endpoint(
  path: '/api/users',
  method: 'POST',
  summary: 'Create user',
  tags: ['Users'],
  security: [{'BearerAuth': []}],
  statusCode: 201,
  contentTypes: ['application/json'],
)
class CreateUserEndpoint extends SparkEndpointWithBody<CreateUserDto> {
  @override
  Future<UserDto> handler(SparkRequest request, CreateUserDto body) async {
    // CreateUserDto auto-documented as request body
    // UserDto auto-documented as response
    final user = await userService.create(body);
    return UserDto(name: user.name);
  }
}

Run `spark openapi` to generate the specification file, then use tools like Swagger UI or Redoc to view the interactive documentation.