dartapi_core 0.0.17 copy "dartapi_core: ^0.0.17" to clipboard
dartapi_core: ^0.0.17 copied to clipboard

Core utilities for building typed, structured REST APIs in Dart, including routing, validation, and middleware support.

dartapi_core #

Core utilities for building typed, structured REST APIs in Dart — routing, validation, middleware, and OpenAPI documentation.

Part of the DartAPI ecosystem.


Installation #

dependencies:
  dartapi_core: ^0.0.11

Routing #

Define endpoints with ApiRoute<Input, Output>. The framework handles request parsing, response serialization, and error mapping automatically.

class UserController extends BaseController {
  @override
  List<ApiRoute> get routes => [
    ApiRoute<void, List<String>>(
      method: ApiMethod.get,
      path: '/users',
      typedHandler: getAllUsers,
      summary: 'Get all users',
    ),
    ApiRoute<UserDTO, UserDTO>(
      method: ApiMethod.post,
      path: '/users',
      statusCode: 201,
      typedHandler: createUser,
      dtoParser: UserDTO.fromJson,
    ),
  ];
}

Path Parameters #

Use request.pathParam<T>(name) to extract typed path parameters. Shelf Router populates these from route patterns like /users/<id>.

ApiRoute<void, User>(
  method: ApiMethod.get,
  path: '/users/<id>',
  typedHandler: (request, _) async {
    final id = request.pathParam<int>('id');
    return userService.findById(id);
  },
)

Supported types: String, int, double, bool. Throws ApiException(400) if the param is missing or cannot be converted.


Query Parameters #

Use request.queryParam<T>(name, defaultValue: ...) to extract typed query parameters.

ApiRoute<void, List<Product>>(
  method: ApiMethod.get,
  path: '/products',
  typedHandler: (request, _) async {
    final page = request.queryParam<int>('page', defaultValue: 1);
    final limit = request.queryParam<int>('limit', defaultValue: 20);
    final search = request.queryParam<String>('q');
    return productService.list(page: page!, limit: limit!, search: search);
  },
)

Returns null (or defaultValue) when the parameter is absent.


Custom Response Status Codes #

Set statusCode on any route to override the default 200 OK:

ApiRoute(method: ApiMethod.post,   path: '/users',      statusCode: 201, ...)
ApiRoute(method: ApiMethod.delete, path: '/users/<id>', statusCode: 204, ...)

Request Validation #

Use verifyKey<T>() on request body maps to extract fields with type checking and optional validators:

factory UserDTO.fromJson(Map<String, dynamic> json) {
  return UserDTO(
    name:  json.verifyKey<String>('name'),
    age:   json.verifyKey<int>('age'),
    email: json.verifyKey<String>('email', validators: [
      EmailValidator('Invalid email'),
    ]),
  );
}

Throws ApiException(422) on missing fields, wrong types, or failed validation. Type errors report friendly names (string, integer, boolean) rather than Dart type names.

Built-in validators #

Validator Description
EmailValidator(message) Validates email format

Custom validators #

class MinLengthValidator extends Validators<String> {
  final int min;
  MinLengthValidator(this.min) : super('Must be at least $min characters');

  @override
  bool validate(dynamic value) => (value as String).length >= min;
}

Error Handling #

Throw ApiException(statusCode, message) from any handler or validator to return a specific HTTP error:

throw ApiException(404, 'User not found');
throw ApiException(422, 'Invalid input');
throw ApiException(401, 'Unauthorized');

The framework catches these automatically and returns a JSON response with the correct status code.


Middleware #

Logging (built-in) #

Pipeline().addMiddleware(loggingMiddleware())

Logs method, URI, and response status for every request.

Global exception handler #

Catch any unhandled exception and return a controlled error response:

Pipeline()
  .addMiddleware(globalExceptionMiddleware(
    onError: (error, stackTrace) {
      if (error is DatabaseException) return ApiException(503, 'Database unavailable');
      return ApiException(500, 'Something went wrong');
    },
  ))
  .addMiddleware(loggingMiddleware())
  .addHandler(router.handler)

Per-route middleware #

ApiRoute(
  middlewares: [authMiddleware(jwtService)],
  ...
)

Rate limiting #

Token-bucket limiter keyed by client IP by default. Returns 429 Too Many Requests with Retry-After and X-RateLimit-* headers when the bucket is empty.

Pipeline()
  .addMiddleware(rateLimitMiddleware(
    maxRequests: 100,
    window: Duration(minutes: 1),
  ))
  .addHandler(router.handler)

Key by user ID or API key instead of IP:

rateLimitMiddleware(
  maxRequests: 1000,
  keyExtractor: (req) =>
      (req.context['user'] as Map?)?['sub'] as String? ?? 'anonymous',
)

Request ID #

Attaches X-Request-Id to every request/response. Propagates an existing ID from the client if present; otherwise generates a new random one. The ID is also stored in request.context['requestId'].

Pipeline()
  .addMiddleware(requestIdMiddleware())
  .addHandler(router.handler)

Response compression #

Gzip-compresses responses when the client sends Accept-Encoding: gzip and the body exceeds a configurable threshold (default 1 KB).

Pipeline()
  .addMiddleware(compressionMiddleware())           // default threshold: 1024 bytes
  .addMiddleware(compressionMiddleware(threshold: 512))
  .addHandler(router.handler)

OpenAPI / Swagger Docs #

Call enableDocs() after addControllers() to serve auto-generated documentation:

app.addControllers([userController, productController]);
app.enableDocs(title: 'My App', version: '1.0.0');
await app.start();

This registers three endpoints:

Endpoint Description
GET /openapi.json OpenAPI 3.0 spec
GET /docs Swagger UI (with persistent Bearer token support)
GET /redoc ReDoc UI

Mark routes that require authentication so Swagger UI shows the lock icon:

ApiRoute(
  method: ApiMethod.get,
  path: '/me',
  security: [SecurityScheme.bearer],
  middlewares: [authMiddleware(jwtService)],
  typedHandler: getProfile,
)

Export the spec from the CLI while the server is running:

dartapi docs --out openapi.json

File Uploads #

Parse multipart/form-data requests using the Request extensions added by this package.

Future<String> uploadAvatar(Request request, void _) async {
  if (!request.isMultipart) throw ApiException(400, 'Expected multipart/form-data');

  final avatar = await request.file('avatar');
  if (avatar == null) throw ApiException(400, 'Missing file field "avatar"');

  // avatar.bytes, avatar.filename, avatar.contentType
  await saveFile(avatar.filename!, avatar.bytes);
  return 'Uploaded ${avatar.filename}';
}

Read plain form fields alongside files:

final fields = await request.formFields();  // Map<String, String>
final title = fields['title'] ?? '';

final parts = await request.multipartFiles(); // List<UploadedFile>

Background Tasks #

Schedule async work to run after the response has been sent (similar to FastAPI's BackgroundTasks). Add backgroundTaskMiddleware() to the pipeline once, then call request.backgroundTasks.add(...) from any handler.

// In pipeline setup:
Pipeline()
  .addMiddleware(backgroundTaskMiddleware())
  .addHandler(router.handler)

// In a handler:
Future<String> createUser(Request request, UserDTO? dto) async {
  request.backgroundTasks.add(() => emailService.sendWelcome(dto!.email));
  return 'User created';  // response sent immediately; email sends after
}

Tasks run sequentially after the response resolves. Errors in tasks are swallowed so they never affect the response.


WebSocket Support #

Define WebSocket endpoints in a controller alongside HTTP routes:

class ChatController extends BaseController {
  @override
  List<ApiRoute> get routes => [];

  @override
  List<WebSocketRoute> get webSocketRoutes => [
    WebSocketRoute(
      path: '/ws/chat',
      handler: (channel, _) async {
        await for (final message in channel.stream) {
          channel.sink.add('Echo: $message');
        }
      },
    ),
  ];
}

Apply middleware (e.g. auth) before the upgrade handshake:

WebSocketRoute(
  path: '/ws/private',
  middlewares: [authMiddleware(jwtService)],
  handler: (channel, _) { ... },
)

The generated RouterManager handles both routes and webSocketRoutes automatically.


Validation #

Built-in validators #

Validator Type Description
EmailValidator(message) String Validates email format
MinLengthValidator(n) String At least n characters
MaxLengthValidator(n) String At most n characters
NotEmptyValidator() String Non-blank string
RangeValidator<T>(min:, max:) num Numeric range (inclusive)
PatternValidator(regex, message) String Regex match
UrlValidator() String Valid http/https URL
factory UserDTO.fromJson(Map<String, dynamic> json) => UserDTO(
  name: json.verifyKey<String>('name', validators: [
    MinLengthValidator(2),
    MaxLengthValidator(50),
  ]),
  age: json.verifyKey<int>('age', validators: [
    RangeValidator<int>(min: 0, max: 150),
  ]),
  website: json.verifyKey<String>('website', validators: [UrlValidator()]),
);

Pagination #

Pagination.fromRequest() reads ?page and ?limit, clamps them, and computes the SQL offset:

final p = Pagination.fromRequest(request, defaultLimit: 20, maxLimit: 100);
final rows = await db.select('products', limit: p.limit, offset: p.offset);

Wrap results in PaginatedResponse for a consistent envelope:

return PaginatedResponse(data: rows, pagination: p, total: totalCount);

Serializes to:

{
  "data": [...],
  "meta": { "page": 2, "limit": 20, "total": 150, "totalPages": 8, "hasNext": true, "hasPrev": true }
}

Server-Sent Events #

Stream events to the client with sseResponse(). The response sets the correct headers automatically.

ApiRoute<void, void>(
  method: ApiMethod.get,
  path: '/events',
  typedHandler: (req, _) async {
    final stream = Stream.periodic(Duration(seconds: 1), (i) =>
      SseEvent(data: 'tick $i', event: 'tick', id: '$i'));
    return sseResponse(stream.take(10));
  },
)

SseEvent fields: data (required), id, event, retry.


Headers #

Use request.header<T>(name) to extract typed request headers (case-insensitive):

final locale = request.header<String>('Accept-Language');
final version = request.header<int>('X-Api-Version', defaultValue: 1);

Returns null (or defaultValue) when absent. Throws ApiException(400) if the value cannot be cast.


Cookies #

Read cookies from incoming requests:

final all = request.cookies;          // Map<String, String>
final token = request.cookie('session'); // String?

Set cookies on a response:

return setCookie(
  Response.ok('logged in'),
  'session', tokenValue,
  maxAge: Duration(hours: 1),
  httpOnly: true,
  secure: true,
  sameSite: 'Strict',
);

Response Caching #

Cache GET responses in memory for a configurable TTL. Cached responses include X-Cache: HIT; cache misses include X-Cache: MISS.

Pipeline()
  .addMiddleware(cacheMiddleware(ttl: Duration(minutes: 10)))
  .addHandler(router.handler)

Use a custom key extractor to ignore query parameters or key by user:

cacheMiddleware(
  ttl: Duration(minutes: 5),
  keyExtractor: (req) => req.url.path,
)

Only 200 responses are cached. POST/PUT/DELETE/PATCH requests bypass the cache entirely.



License #

BSD 3-Clause License © 2025 Akash G Krishnan

1
likes
0
points
1.2k
downloads

Publisher

verified publisherakashgk.com

Weekly Downloads

Core utilities for building typed, structured REST APIs in Dart, including routing, validation, and middleware support.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

mime, shelf, shelf_web_socket, web_socket_channel

More

Packages that depend on dartapi_core