npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@geekmidas/api

v0.1.0

Published

A comprehensive REST API framework for building type-safe HTTP endpoints with built-in support for AWS Lambda, validation, error handling, and service dependency injection.

Downloads

237

Readme

@geekmidas/api

A comprehensive REST API framework for building type-safe HTTP endpoints with built-in support for AWS Lambda, validation, error handling, and service dependency injection.

Features

  • 🔒 Type-safe endpoints: Full TypeScript support with automatic type inference
  • Schema validation: Uses StandardSchema specification (Zod, Valibot, etc.)
  • 🚀 Multiple runtime support: AWS Lambda adapters and test adapter included
  • 💉 Dependency injection: Built-in service discovery and registration
  • 🔐 Authorization: Flexible authorization system with session management
  • 📄 OpenAPI generation: Automatic OpenAPI schema generation with reusable components
  • 🚨 Error handling: Comprehensive HTTP error classes and handling
  • 📊 Structured logging: Built-in logger with context propagation
  • 📡 Event publishing: Automatic event publishing after successful endpoint execution
  • 🎯 Zero config: Works out of the box with sensible defaults
  • 🔍 Advanced query parameters: Support for nested objects and arrays in query strings
  • ♾️ Infinite queries: Built-in React Query infinite pagination support
  • 🪝 OpenAPI hooks: Generate type-safe hooks from operation IDs

Available Adapters

  • AmazonApiGatewayV1Endpoint: For AWS API Gateway v1 (REST API)
  • AmazonApiGatewayV2Endpoint: For AWS API Gateway v2 (HTTP API)
  • TestEndpointAdaptor: For unit testing endpoints

Installation

npm install @geekmidas/api zod
# or
yarn add @geekmidas/api zod
# or
pnpm add @geekmidas/api zod

For AWS Lambda support:

npm install @geekmidas/api @geekmidas/envkit @types/aws-lambda aws-lambda

Quick Start

Basic Endpoint

import { e } from '@geekmidas/api/server';
import { z } from 'zod';

// Define a simple GET endpoint
const getUser = e
  .get('/users/:id')
  .params(z.object({ id: z.string().uuid() }))
  .output(z.object({
    id: z.string(),
    name: z.string(),
    email: z.string().email()
  }))
  .handle(async ({ params, logger }) => {
    logger.info({ userId: params.id }, 'Fetching user');
    
    // Your logic here
    return {
      id: params.id,
      name: 'John Doe',
      email: '[email protected]'
    };
  });

AWS Lambda Handler

import { AmazonApiGatewayV1Endpoint } from '@geekmidas/api/aws-apigateway';
import { EnvironmentParser } from '@geekmidas/envkit';

const envParser = new EnvironmentParser(process.env);

const endpoint = e
  .get('/health')
  .output(z.object({ status: z.string() }))
  .handle(() => ({ status: 'ok' }));

const adapter = new AmazonApiGatewayV1Endpoint(envParser, endpoint);
export const handler = adapter.handler;

API Gateway v2 (HTTP API)

For AWS API Gateway v2 (HTTP APIs), use the AmazonApiGatewayV2Endpoint adapter:

import { AmazonApiGatewayV2Endpoint } from '@geekmidas/api/aws-apigateway';
import { EnvironmentParser } from '@geekmidas/envkit';

const envParser = new EnvironmentParser(process.env);

const endpoint = e
  .post('/users')
  .body(z.object({ name: z.string(), email: z.string().email() }))
  .output(z.object({ id: z.string(), created: z.boolean() }))
  .handle(async ({ body }) => ({
    id: crypto.randomUUID(),
    created: true
  }));

const adapter = new AmazonApiGatewayV2Endpoint(envParser, endpoint);
export const handler = adapter.handler;

The v2 adapter automatically handles the differences in the API Gateway v2 event format, including:

  • Different request context structure
  • Simplified path and query parameter handling
  • HTTP API-specific features

Core Concepts

Endpoint vs Function

This package provides two main constructs:

  1. RestEndpoint (e export) - For HTTP REST APIs

    • Handler receives destructured parameters: { params, query, body, headers, services, logger, session }
    • Designed for web services with HTTP-specific concepts
  2. Function - For general-purpose functions

    • Handler receives an input object: { input, services, logger }
    • More generic construct for non-HTTP use cases

Endpoint Builder

The endpoint builder provides a fluent API for defining HTTP endpoints:

import { e } from '@geekmidas/api/server';
import { z } from 'zod';

const endpoint = e
  .post('/users')                                    // HTTP method and path
  .params(z.object({ orgId: z.string() }))          // URL parameters
  .query(z.object({ role: z.string().optional() })) // Query parameters
  .headers(z.object({ 'x-api-key': z.string() }))   // Headers validation
  .body(z.object({                                   // Request body
    name: z.string(),
    email: z.string().email()
  }))
  .output(z.object({                                 // Response schema
    id: z.string(),
    name: z.string(),
    email: z.string()
  }))
  .handle(async ({ params, query, headers, body, logger }) => {
    // Your endpoint logic
    return { id: '123', ...body };
  });

Service Dependency Injection

Define and use services across your endpoints:

import { HermodService } from '@geekmidas/api/server';

// Define a service
class DatabaseService extends HermodService<Database> {
  static readonly serviceName = 'Database';
  
  async register() {
    const db = new Database(process.env.DATABASE_URL);
    await db.connect();
    return db;
  }
  
  async cleanup(db: Database) {
    await db.disconnect();
  }
}

// Use in endpoints
const endpoint = e
  .services([DatabaseService])
  .post('/users')
  .body(userSchema)
  .handle(async ({ body, services }) => {
    const db = services.Database;
    const user = await db.users.create(body);
    return user;
  });

Error Handling

Comprehensive error handling with HTTP-specific error classes:

import { 
  NotFoundError, 
  BadRequestError,
  UnauthorizedError,
  createError,
  createHttpError 
} from '@geekmidas/api/errors';

const endpoint = e
  .get('/users/:id')
  .handle(async ({ params }) => {
    const user = await findUser(params.id);
    
    if (!user) {
      throw new NotFoundError('User not found');
    }
    
    if (!user.isActive) {
      throw createError.forbidden('User account is inactive');
    }
    
    // Custom error with additional data
    throw createHttpError(422, 'Validation failed', {
      validationErrors: {
        email: 'Invalid format'
      }
    });
    
    return user;
  });

Route Grouping

Organize endpoints with route prefixes:

// Create API version prefix
const api = e.route('/api/v1');

// Group user endpoints
const users = api.route('/users');

const listUsers = users.get('/').handle(/* ... */);        // GET /api/v1/users
const getUser = users.get('/:id').handle(/* ... */);       // GET /api/v1/users/:id
const createUser = users.post('/').handle(/* ... */);      // POST /api/v1/users
const updateUser = users.put('/:id').handle(/* ... */);    // PUT /api/v1/users/:id

// Group admin endpoints
const admin = api.route('/admin');
const getStats = admin.get('/stats').handle(/* ... */);    // GET /api/v1/admin/stats

Authorization

Implement authorization at the route or endpoint level:

// Route-level authorization
const protectedApi = e.authorize(async ({ req, logger }) => {
  const token = req.headers.get('authorization');
  
  if (!token) {
    return false; // Unauthorized
  }
  
  try {
    const user = await verifyToken(token);
    return { userId: user.id }; // Pass data to endpoints
  } catch {
    return false;
  }
});

// All endpoints under protectedApi require authorization
const endpoint = protectedApi
  .get('/profile')
  .handle(({ auth }) => {
    // auth contains { userId: string }
    return { userId: auth.userId };
  });

// Endpoint-specific authorization
const adminEndpoint = e
  .get('/admin/users')
  .authorize(async ({ req }) => {
    const user = await getUser(req);
    return user.role === 'admin';
  })
  .handle(() => {
    // Only admins can access
  });

Session Management

Add session data to all endpoints:

interface SessionData {
  userId: string;
  organizationId: string;
}

const api = e.session<SessionData>(async ({ req }) => {
  const token = req.headers.get('authorization');
  
  if (!token) {
    return null;
  }
  
  return await decodeSessionToken(token);
});

const endpoint = api
  .get('/my-organization')
  .handle(({ session }) => {
    // session is SessionData | null
    if (!session) {
      throw new UnauthorizedError();
    }
    
    return { orgId: session.organizationId };
  });

Advanced Query Parameters

The framework supports complex query parameter structures including nested objects and arrays:

const endpoint = e
  .get('/users')
  .query(z.object({
    // Simple parameters
    page: z.number().optional(),
    limit: z.number().optional(),
    
    // Arrays
    ids: z.array(z.string()).optional(),
    
    // Nested objects using dot notation
    'filter.status': z.enum(['active', 'inactive']).optional(),
    'filter.role': z.string().optional(),
    
    // Nested arrays
    'user.roles': z.array(z.string()).optional()
  }))
  .handle(async ({ query }) => {
    // Query: ?ids=1&ids=2&filter.status=active&user.roles=admin&user.roles=moderator
    // Parsed as:
    // {
    //   ids: ['1', '2'],
    //   filter: { status: 'active' },
    //   user: { roles: ['admin', 'moderator'] }
    // }
    return queryUsers(query);
  });

Logging

Built-in structured logging with context:

const endpoint = e
  .get('/users/:id')
  .handle(async ({ params, logger }) => {
    // Logger includes request context
    logger.info({ userId: params.id }, 'Fetching user');
    
    try {
      const user = await getUser(params.id);
      logger.debug({ user }, 'User found');
      return user;
    } catch (error) {
      logger.error({ error, userId: params.id }, 'Failed to fetch user');
      throw error;
    }
  });

Advanced Usage

Custom Services

Create reusable services for your endpoints:

// Cache service
class CacheService extends HermodService<Redis> {
  static readonly serviceName = 'Cache';
  
  async register() {
    const redis = new Redis(process.env.REDIS_URL);
    return redis;
  }
}

// Email service
class EmailService extends HermodService<EmailClient> {
  static readonly serviceName = 'Email';
  
  async register() {
    return new EmailClient({
      apiKey: process.env.SENDGRID_API_KEY
    });
  }
}

// Use multiple services
const endpoint = e
  .services([DatabaseService, CacheService, EmailService])
  .post('/users')
  .handle(async ({ input, services }) => {
    const { Database, Cache, Email } = services;
    
    // Check cache first
    const cached = await Cache.get(`user:${input.body.email}`);
    if (cached) return cached;
    
    // Create user
    const user = await Database.users.create(input.body);
    
    // Cache result
    await Cache.set(`user:${user.email}`, user, 3600);
    
    // Send welcome email
    await Email.send({
      to: user.email,
      subject: 'Welcome!',
      body: 'Thanks for signing up!'
    });
    
    return user;
  });

OpenAPI Generation

Generate OpenAPI schemas from your endpoints:

Basic OpenAPI Generation

const endpoint = e
  .get('/users/:id')
  .params(z.object({ id: z.string().uuid() }))
  .query(z.object({ 
    include: z.enum(['profile', 'posts']).optional() 
  }))
  .output(z.object({
    id: z.string(),
    name: z.string(),
    email: z.string().email(),
    profile: z.object({
      bio: z.string(),
      avatar: z.string().url()
    }).optional()
  }))
  .openapi({
    summary: 'Get user by ID',
    description: 'Retrieves a user by their unique identifier',
    tags: ['Users'],
    security: [{ bearerAuth: [] }]
  })
  .handle(async ({ params, query }) => {
    // Implementation
  });

// Generate OpenAPI document
const openApiDoc = generateOpenApiDocument([endpoint]);

OpenAPI Components (Reusable Schemas)

You can extract schemas to the OpenAPI components section for reuse:

import { z } from 'zod';

// Mark schema for extraction to components
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email()
}).describe('User').openapi('User');

const ProfileSchema = z.object({
  bio: z.string(),
  avatar: z.string().url()
}).describe('UserProfile').openapi('UserProfile');

// Use in endpoints - will generate $ref in OpenAPI
const endpoint = e
  .get('/users/:id')
  .output(UserSchema.extend({
    profile: ProfileSchema.optional()
  }))
  .handle(async ({ params }) => {
    // Implementation
  });

// Generated OpenAPI will include:
// components:
//   schemas:
//     User:
//       type: object
//       properties: ...
//     UserProfile:
//       type: object
//       properties: ...

Middleware Pattern

While the framework doesn't have traditional middleware, you can compose functionality:

// Create a base endpoint with common functionality
const authenticatedApi = e
  .session(getSession)
  .authorize(requireAuth)
  .services([DatabaseService, LoggerService]);

// Build on top of the base
const userEndpoints = authenticatedApi.route('/users');

const getProfile = userEndpoints
  .get('/profile')
  .handle(({ session, services }) => {
    // Has session, auth, and services from base
  });

Testing

The framework provides a dedicated test adapter for unit testing endpoints:

import { TestEndpointAdaptor } from '@geekmidas/api/testing';
import { EnvironmentParser } from '@geekmidas/envkit';

describe('User endpoint', () => {
  const envParser = new EnvironmentParser({});
  
  it('should return user by ID', async () => {
    const endpoint = e
      .get('/users/:id')
      .params(z.object({ id: z.string() }))
      .output(userSchema)
      .handle(async ({ params }) => {
        return { id: params.id, name: 'Test User' };
      });
    
    const adapter = new TestEndpointAdaptor(envParser, endpoint);
    const response = await adapter.request({
      method: 'GET',
      url: '/users/123'
    });
    
    expect(response.status).toBe(200);
    expect(response.body).toEqual({
      id: '123',
      name: 'Test User'
    });
  });
  
  it('should handle POST requests with body', async () => {
    const createEndpoint = e
      .post('/users')
      .body(z.object({ name: z.string(), email: z.string().email() }))
      .output(z.object({ id: z.string(), name: z.string() }))
      .handle(async ({ body }) => ({
        id: '123',
        name: body.name
      }));
    
    const adapter = new TestEndpointAdaptor(envParser, createEndpoint);
    const response = await adapter.request({
      method: 'POST',
      url: '/users',
      body: { name: 'John Doe', email: '[email protected]' }
    });
    
    expect(response.status).toBe(200);
    expect(response.body.name).toBe('John Doe');
  });
});

Typed API Client

A fully type-safe API client for TypeScript that uses OpenAPI specifications to provide automatic type inference for requests and responses.

Features

  • 🚀 Full TypeScript support with automatic type inference
  • 🔒 Type-safe request parameters (path, query, body)
  • 📦 Built-in React Query integration
  • 🛡️ Request/response interceptors
  • 🔄 Automatic OpenAPI types generation
  • 💪 Zero runtime overhead - all types are compile-time only

Installation

npm install @geekmidas/api
# or
pnpm add @geekmidas/api

Quick Start

1. Generate Types from OpenAPI Spec

First, generate TypeScript types from your OpenAPI specification using openapi-typescript:

# Install openapi-typescript
npm install -D openapi-typescript

# Generate types from URL
npx openapi-typescript https://api.example.com/openapi.json -o ./src/openapi-types.d.ts

# Or generate types from local file
npx openapi-typescript ./openapi.yaml -o ./src/openapi-types.d.ts

This will create a file with your API types that looks like:

export interface paths {
  "/users": {
    get: {
      responses: {
        200: {
          content: {
            "application/json": User[];
          };
        };
      };
    };
    post: {
      requestBody: {
        content: {
          "application/json": {
            name: string;
            email: string;
          };
        };
      };
      responses: {
        201: {
          content: {
            "application/json": User;
          };
        };
      };
    };
  };
  // ... more endpoints
}

2. Create a Typed Fetcher

import { createTypedFetcher } from '@geekmidas/api/client';
import type { paths } from './openapi-types';

const client = createTypedFetcher<paths>({
  baseURL: 'https://api.example.com',
  headers: {
    'Authorization': 'Bearer your-token',
  },
});

// TypeScript automatically infers the response type!
const user = await client('GET /users/{id}', {
  params: { id: '123' },
});

console.log(user.name); // TypeScript knows this is a string

3. Use with React Query

import { createTypedQueryClient } from '@geekmidas/api/client';
import type { paths } from './openapi-types';

const queryClient = createTypedQueryClient<paths>({
  baseURL: 'https://api.example.com',
});

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading } = queryClient.useQuery(
    'GET /users/{id}',
    { params: { id: userId } }
  );
  
  if (isLoading) return <div>Loading...</div>;
  
  // TypeScript knows user has properties: id, name, email
  return <div>{user?.name}</div>;
}

// Query Invalidation
async function refreshUser(userId: string) {
  // Invalidate specific user query
  await queryClient.invalidateQueries('GET /users/{id}', {
    params: { id: userId }
  });
  
  // Invalidate all user queries
  await queryClient.invalidateQueries('GET /users');
  
  // Invalidate all queries
  await queryClient.invalidateAllQueries();
}

// Mutations
function CreateUser() {
  const { mutate: createUser } = queryClient.useMutation(
    'POST /users',
    {
      onSuccess: (data) => {
        console.log('User created:', data);
      }
    }
  );
  
  const handleSubmit = () => {
    createUser({
      body: { name: 'New User', email: '[email protected]' }
    });
  };
  
  return <button onClick={handleSubmit}>Create User</button>;
}

API Reference

createTypedFetcher<Paths>(options)

Creates a typed fetcher instance.

Type Parameters

  • Paths: Your OpenAPI paths type (generated from your OpenAPI spec)

Options

  • baseURL: Base URL for all requests
  • headers: Default headers to include with every request
  • onRequest: Request interceptor
  • onResponse: Response interceptor
  • onError: Error handler

createTypedQueryClient<Paths>(options)

Creates a typed React Query client.

Type Parameters

  • Paths: Your OpenAPI paths type (generated from your OpenAPI spec)

Options

Extends FetcherOptions

Request Configuration

The second parameter accepts a configuration object with the following properties (only available properties based on the endpoint will be accepted):

  • params: Path parameters (e.g., {id} in /users/{id})
  • query: Query parameters
  • body: Request body (for POST, PUT, PATCH requests)
  • headers: Additional headers for this request

Advanced Usage

Interceptors

import type { paths } from './your-openapi-types';

const client = createTypedFetcher<paths>({
  baseURL: 'https://api.example.com',
  onRequest: async (config) => {
    // Modify request before sending
    config.headers['X-Request-ID'] = generateRequestId();
    return config;
  },
  onResponse: async (response) => {
    // Process response
    if (response.headers.get('X-Refresh-Token')) {
      await refreshAuth();
    }
    return response;
  },
  onError: async (error) => {
    // Handle errors globally
    if (error.response?.status === 401) {
      window.location.href = '/login';
    }
  },
});

Infinite Queries (Pagination)

The client supports React Query's infinite queries for pagination:

function PostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = queryClient.useInfiniteQuery(
    'GET /posts',
    {
      query: { limit: 20 }, // Will be merged with pageParam
      getNextPageParam: (lastPage) => lastPage.nextCursor,
      initialPageParam: undefined,
    }
  );

  return (
    <div>
      {data?.pages.map((page) =>
        page.items.map((post) => <PostCard key={post.id} post={post} />)
      )}
      
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

OpenAPI Hooks (Operation ID-based)

Generate hooks using OpenAPI operation IDs for better organization:

import { createOpenAPIHooks } from '@geekmidas/api/client';
import type { paths } from './openapi-types';

// Create hooks based on operation IDs
const api = createOpenAPIHooks<paths>({
  baseURL: 'https://api.example.com',
});

// Use hooks with operation IDs
function UserProfile() {
  // Assumes your OpenAPI spec has operationId: "getUser"
  const { data: user } = api.useGetUser({
    params: { id: '123' }
  });
  
  // Assumes operationId: "updateUser"
  const { mutate: updateUser } = api.useUpdateUser();
  
  return <div>{user?.name}</div>;
}

Type-Safe Error Handling

try {
  const user = await client('GET /users/{id}', {
    params: { id: userId },
  });
  // Handle success
} catch (error) {
  if (error.response?.status === 404) {
    // User not found
  }
}

How It Works

  1. OpenAPI Types: The openapi-typescript tool generates TypeScript interfaces from your OpenAPI spec
  2. Type Magic: Our client uses TypeScript's template literal types and conditional types to:
    • Parse the endpoint string (e.g., 'GET /users/{id}')
    • Extract the HTTP method and path
    • Look up the corresponding types from the OpenAPI definitions
    • Infer request parameters and response types
    • Provide VS Code autocomplete for all valid endpoints
  3. Runtime Fetching: At runtime, the client constructs and executes the HTTP request

VS Code Autocomplete

When you type endpoint strings, you get full autocomplete showing all available endpoints:

// Start typing: client('
// VS Code shows:
//   ✓ 'GET /users'
//   ✓ 'POST /users' 
//   ✓ 'GET /users/{id}'
//   ✓ 'PUT /users/{id}'
//   ✓ 'DELETE /users/{id}'
//   ✓ 'GET /posts'

const user = await client('GET /users/{id}', {
  params: { id: '123' }  // ← TypeScript enforces required params
});

Best Practices

  1. Keep OpenAPI Spec Updated: Regenerate types whenever your API changes
    npx openapi-typescript https://api.example.com/openapi.json -o ./src/openapi-types.d.ts
  2. Use Specific Endpoints: Let TypeScript autocomplete guide you to valid endpoints
  3. Handle Errors: Always handle potential errors, especially for mutations
  4. Cache Wisely: Configure React Query's staleTime and cacheTime appropriately
  5. Commit Generated Types: Include the generated types file in your repository for team consistency

TypeScript Support

This library requires TypeScript 4.5+ for full template literal type support.

License

MIT

Error Reference

The framework provides a comprehensive set of HTTP error classes:

| Error Class | Status Code | Usage | |------------|-------------|--------| | BadRequestError | 400 | Invalid request data | | UnauthorizedError | 401 | Missing or invalid authentication | | PaymentRequiredError | 402 | Payment required | | ForbiddenError | 403 | Authenticated but not authorized | | NotFoundError | 404 | Resource not found | | MethodNotAllowedError | 405 | HTTP method not allowed | | ConflictError | 409 | Resource conflict | | UnprocessableEntityError | 422 | Validation errors | | TooManyRequestsError | 429 | Rate limit exceeded | | InternalServerError | 500 | Server errors | | BadGatewayError | 502 | Gateway errors | | ServiceUnavailableError | 503 | Service unavailable |

Documentation

Event Publishing

The framework includes powerful event publishing capabilities for building event-driven architectures:

const createUserEndpoint = e
  .publisher(eventPublisher)
  .post('/users')
  .body(userSchema)
  .event({
    type: 'user.created',
    payload: (response) => ({ userId: response.id, email: response.email }),
  })
  .handle(async ({ body }) => {
    const user = await createUser(body);
    return user; // Event published automatically after success
  });

Best Practices

  1. Use structured services: Keep endpoint handlers thin by moving logic to services
  2. Validate everything: Use schemas for params, query, headers, body, and output
  3. Handle errors gracefully: Use appropriate HTTP error classes
  4. Log strategically: Use structured logging with context
  5. Group related endpoints: Use route prefixes to organize your API
  6. Document with OpenAPI: Add OpenAPI metadata to your endpoints
  7. Test thoroughly: Use the testing utilities to test endpoints in isolation
  8. Publish events wisely: Use events for cross-service communication and audit trails

License

MIT