Skip to main content

API Structure, Versioning & Backwards Compatibility

Overview

The Ever Works API is a NestJS 11 REST API that uses a flat route prefix (api/) with domain-based controller grouping. All routes are documented via OpenAPI/Swagger with Scalar API Reference. The API enforces authentication globally via JwtAuthGuard, applies rate limiting through tiered throttling, uses a global ValidationPipe for DTO validation, and registers interceptors for logging, Sentry error tracking, and PostHog analytics. This page documents the API's structural conventions, route organization, middleware stack, and approach to compatibility.

Architecture

Source Files

FilePurpose
apps/api/src/main.tsApplication bootstrap: Helmet, CORS, ValidationPipe, Swagger, Scalar
apps/api/src/api.module.tsRoot module: guards, interceptors, module imports, plugin bootstrap
apps/api/src/api.controller.tsHealth check endpoints (/, /api/health)
apps/api/src/config/throttler.config.tsTiered rate limiting configuration
apps/api/src/logging.interceptor.tsDebug-mode HTTP request/response logging
apps/api/src/works/works.controller.tsPrimary domain controller (works, items, taxonomy)
apps/api/src/auth/controllers/auth.controller.tsAuthentication (login, register, password reset)
apps/api/src/auth/controllers/api-keys.controller.tsAPI key management
apps/api/src/plugins/plugins.controller.tsPlugin enable/disable and settings management

Key Classes

Application Bootstrap (main.ts)

The API application configures the full middleware and documentation stack at startup:

async function bootstrap() {
const app = await NestFactory.create(ApiModule);

// Body parser limits
app.use(json({ limit: '10mb' }));
app.use(urlencoded({ limit: '10mb', extended: true }));

// Security: Helmet with relaxed CSP for API docs
app.use((req, res, next) => {
if (req.path.startsWith('/api/docs')) {
return helmet({
contentSecurityPolicy: {
/* relaxed */
}
})(req, res, next);
}
return helmet()(req, res, next);
});

// CORS
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
});

// Global validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true
})
);

// OpenAPI documentation
const config = new DocumentBuilder()
.setTitle('Ever Works API')
.setVersion('1.0')
.addBearerAuth(/* JWT config */)
.addTag('Health')
.addTag('Auth')
.addTag('Works') /* ... */
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/swagger', app, document);

// Scalar API Reference at /api/docs
app.use('/api/docs', apiReference({ url: '/api/openapi.json', theme: 'kepler' }));

await app.listen(process.env.PORT ?? 3100);
}

Global Guard & Interceptor Stack

Registered in ApiModule via APP_GUARD and APP_INTERCEPTOR tokens:

@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, // Authentication
{ provide: APP_GUARD, useClass: ThrottlerGuard }, // Rate limiting
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, // Debug logging
{ provide: APP_INTERCEPTOR, useClass: SentryInterceptor }, // Error tracking
{ provide: APP_INTERCEPTOR, useClass: PostHogInterceptor } // Analytics
]
})
export class ApiModule implements OnApplicationBootstrap {
async onApplicationBootstrap(): Promise<void> {
await this.pluginBootstrap.bootstrap(); // Load all plugins
}
}

Controller Route Organization

Controllers use a flat api/ prefix pattern with domain-specific sub-paths:

ControllerRoute PrefixTagAuth
APIController/, /api/healthHealthPublic
AuthControllerapi/authAuthMixed
ApiKeysControllerapi/auth/api-keysAPI KeysJWT
WorksControllerapiWorksJWT
MembersControllerapi/works/:workId/membersMembersJWT
AiConversationControllerapi/ai-conversationsAI ConversationsJWT
PluginsControllerapiPluginsJWT
DeployControllerapi/deployDeployJWT
GitProviderControllerapi/git-providersGit ProvidersJWT
ScreenshotControllerapi/screenshotScreenshotJWT
OAuthControllerapi/oauthOAuthMixed
SubscriptionsControllerapi/subscriptionsSubscriptionsJWT
NotificationsControllerapi/notificationsNotificationsJWT
TriggerInternalControllerinternal/trigger--Internal

Rate Limiting (Throttler)

Three-tier rate limiting is applied globally:

export const throttlerConfig: ThrottlerModuleOptions = {
throttlers: [
{ name: 'short', ttl: 1000, limit: 50 }, // 50 req/sec
{ name: 'medium', ttl: 10000, limit: 300 }, // 300 req/10sec
{ name: 'long', ttl: 60000, limit: 1000 } // 1000 req/min
]
};

LoggingInterceptor

Logs HTTP method, URL, status code, and response time when debug mode is enabled:

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
if (!config.debug()) return next.handle();

const now = Date.now();
const { method, originalUrl } = context.switchToHttp().getRequest();

return next.handle().pipe(
catchError((err) => {
this.logger.error(`Error Response: ${method} ${originalUrl} ${statusCode} - ${delay}ms`);
return throwError(() => err);
}),
tap(() => {
this.logger.log(`Outgoing Response: ${method} ${originalUrl} ${statusCode} - ${delay}ms`);
})
);
}
}

Configuration

Environment Variables

VariablePurposeDefault
PORTAPI server port3100
ALLOWED_ORIGINSComma-separated CORS originshttp://localhost:3000
JWT_SECRETJWT signing secret--
JWT_EXPIRATIONToken expiration7d
SENTRY_DSNSentry error tracking DSN--
POSTHOG_API_KEYPostHog analytics key--
NODE_ENVEnvironment identifierdevelopment

API Documentation Endpoints

EndpointPurpose
/api/docsScalar API Reference (interactive documentation)
/api/swaggerSwagger UI
/api/openapi.jsonOpenAPI 3.0 JSON specification

Code Examples

Typical Controller Pattern

@ApiTags('Works')
@ApiBearerAuth('JWT-auth')
@Controller('api')
@UseGuards(JwtAuthGuard)
export class WorksController {
@Get('works')
@ApiOperation({ summary: 'List works' })
@ApiResponse({ status: 200, description: 'List of works' })
async listWorks(@CurrentUser() user: AuthenticatedUser) {
return this.queryService.findByUser(user.userId);
}

@Post('works')
@ApiOperation({ summary: 'Create work' })
@HttpCode(HttpStatus.CREATED)
async createWork(@Body() dto: CreateWorkDto, @CurrentUser() user: AuthenticatedUser) {
return this.lifecycleService.create(dto, user.userId);
}

@Put('works/:slug')
@ApiOperation({ summary: 'Update work' })
@ApiParam({ name: 'slug', description: 'Work URL slug' })
async updateWork(@Param('slug') slug: string, @Body() dto: UpdateWorkDto, @CurrentUser() user: AuthenticatedUser) {
return this.lifecycleService.update(slug, dto, user.userId);
}
}

Public Route

@Public()
@Get('api/health')
@ApiOperation({ summary: 'Health check' })
healthCheck() {
return { status: 'success', message: 'API is up and running' };
}

Internal Route (No Public Swagger Tag)

@Controller('internal/trigger')
export class TriggerInternalController {
@Post('generation/:slug')
async triggerGeneration(@Param('slug') slug: string) {
// Called by Trigger.dev background jobs
}
}

Dual Authentication (JWT + API Key)

All api/* routes automatically support both authentication methods via the JwtAuthGuard. No controller-level changes are needed:

# JWT authentication
curl -H "Authorization: Bearer eyJhbGciOi..." https://api.example.com/api/works

# API key authentication
curl -H "x-api-key: ew_live_abc123..." https://api.example.com/api/works

Best Practices

  1. Use the api/ prefix -- all public-facing routes use the api/ prefix. Internal routes use internal/. This makes reverse proxy and routing configuration straightforward.

  2. Tag controllers for Swagger -- every controller should have @ApiTags() and every endpoint should have @ApiOperation() with a summary. This generates comprehensive API documentation automatically.

  3. Default to authenticated -- routes require authentication unless explicitly marked with @Public(). This prevents accidental exposure of sensitive endpoints.

  4. Use @ApiBearerAuth('JWT-auth') -- pair this with auth-protected controllers so the Swagger UI shows the lock icon and allows token entry.

  5. Document responses -- use @ApiResponse() decorators to document success and error status codes for each endpoint.

  6. Leverage DTOs from the agent package -- import DTOs from @ever-works/agent/dto to keep validation logic centralized and reusable across controllers.

  7. Use HttpCode for non-200 responses -- NestJS defaults to 200 for POST. Use @HttpCode(HttpStatus.CREATED) for creation endpoints.

  8. Keep controllers thin -- delegate business logic to service classes. Controllers should only handle request/response mapping, validation is handled by the global ValidationPipe.

  9. Monitor with interceptors -- the Sentry and PostHog interceptors automatically capture errors and analytics. Health check routes are filtered to avoid noise.

  10. Use tiered throttling -- the three-tier rate limiter (short/medium/long) protects against both burst and sustained abuse without impacting normal usage.