Skip to main content

API Error Handling

The Ever Works API uses a layered approach to error handling, combining NestJS built-in exception filters, global validation pipes, rate limiting, and structured error responses. This page documents the error handling patterns used throughout the API.

Error Response Format​

All API errors follow a consistent JSON structure:

{
"statusCode": 400,
"message": "Descriptive error message",
"error": "Bad Request"
}

For validation errors, the response includes an array of messages:

{
"statusCode": 400,
"message": ["url must be a URL address", "viewportWidth must not be greater than 3840"],
"error": "Bad Request"
}

Global Validation Pipe​

The API bootstraps with a global ValidationPipe configured in main.ts:

app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
transform: true, // Auto-transform payloads to DTO instances
forbidNonWhitelisted: true // Reject requests with unknown properties
})
);

Validation Behavior​

SettingEffect
whitelist: trueProperties not decorated with validators are silently removed
transform: truePlain objects are transformed to class instances; query string numbers are parsed
forbidNonWhitelisted: trueRequests with properties not defined in the DTO receive a 400 error

Validation Decorators Used​

The API uses class-validator decorators extensively:

DecoratorExample UsageError Message
@IsUrl()Screenshot URL"url must be a URL address"
@IsString()Work ID"workId must be a string"
@IsNumber()Viewport width"viewportWidth must be a number"
@IsBoolean()Full page flag"fullPage must be a boolean value"
@IsOptional()Optional fields(no error if absent)
@Min(n) / @Max(n)Numeric bounds"viewportWidth must not be less than 320"
@IsIn([...])Enum values"format must be one of: png, jpg, webp"
@IsArray()Batch items"works must be an array"
@ValidateNested()Nested DTOsValidates child object properties

HTTP Exception Types​

The API uses standard NestJS HTTP exceptions. Here are the patterns found across the codebase:

BadRequestException (400)​

The most commonly thrown exception, used for invalid user input or misconfigured resources:

// Missing configuration
throw new BadRequestException({
status: 'error',
message: 'No screenshot provider configured'
});

// Invalid credentials
throw new BadRequestException({
status: 'error',
message: 'Vercel token is required. Please configure it in Plugin Settings.'
});

// Missing OAuth code
throw new BadRequestException('Authorization code is required');

// Invalid state
throw new BadRequestException('Invalid state parameter');

HttpException (Various)​

Used by the Twenty CRM integration for proxying upstream errors:

// Proxy upstream CRM error with original status code
throw new HttpException(
{
message: errorData.message || 'Twenty CRM API error',
details: errorData.details
},
error.response.status || HttpStatus.INTERNAL_SERVER_ERROR
);

// Service unavailable
throw new HttpException('Failed to communicate with Twenty CRM', HttpStatus.SERVICE_UNAVAILABLE);

UnauthorizedException (401)​

Thrown automatically by the JwtAuthGuard when no valid JWT is provided, or by Passport when credentials are invalid.

ForbiddenException (403)​

Returned when a guard (e.g., CrmSyncGuard, ThrottlerGuard) blocks the request. The CrmSyncGuard returns false from canActivate(), which NestJS converts to a 403.

Error (500)​

Internal errors thrown within services are not caught by specific exception handlers and result in a generic 500:

throw new Error('Git provider token not available');
throw new Error('GitHub plugin not available for CI/CD operations');

Rate Limiting​

The API uses @nestjs/throttler with three tiers, registered globally:

export const throttlerConfig: ThrottlerModuleOptions = {
throttlers: [
{ name: 'short', ttl: 1000, limit: 50 },
{ name: 'medium', ttl: 10000, limit: 300 },
{ name: 'long', ttl: 60000, limit: 1000 }
]
};
TierWindowMax RequestsUse Case
short1 second50Burst protection
medium10 seconds300Sustained request limiting
long1 minute1000Overall rate cap

The ThrottlerGuard is registered as a global guard via APP_GUARD. When a limit is exceeded, the response is:

{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests"
}

Logging Interceptor Error Handling​

The LoggingInterceptor catches errors in the response pipeline and logs them:

return next.handle().pipe(
catchError((err) => {
const response = err?.response || { statusCode: 500 };
this.logger.error(`Error Response: ${method} ${originalUrl} ${statusCode || 400} - ${delay}ms`);
return throwError(() => err);
})
);

The interceptor only activates when config.debug() returns true. It logs the HTTP method, URL, status code, and response time for both successful and failed requests.

Monitoring Integration​

Two additional interceptors capture errors for external monitoring:

SentryInterceptor​

Captures unhandled exceptions and sends them to Sentry for error tracking. Configured via SENTRY_DSN.

PostHogInterceptor​

Tracks API usage events and errors in PostHog for product analytics. Configured via POSTHOG_API_KEY.

Both are registered as global interceptors in ApiModule:

{ provide: APP_INTERCEPTOR, useClass: SentryInterceptor },
{ provide: APP_INTERCEPTOR, useClass: PostHogInterceptor },

Error Handling Patterns by Module​

Screenshot Capability​

if (!this.screenshotFacade.isAvailable()) {
throw new BadRequestException({
status: 'error',
message: 'No screenshot provider configured'
});
}

Deploy Capability​

if (!isConfigured) {
throw new BadRequestException({
status: 'error',
message: isCreator
? `${providerName} token is required. Configure it in Plugin Settings.`
: `The work owner has not configured ${providerName} credentials.`
});
}

Git Provider / OAuth​

Uses try-catch with graceful degradation:

try {
const organizations = await this.gitProviderService.getOrganizations(...);
return { success: true, organizations };
} catch (error) {
return {
success: false,
organizations: [],
error: error instanceof Error ? error.message : 'Failed to fetch organizations',
};
}

CRM Integration​

Structured error forwarding with details:

throw new HttpException(
{
message: errorData.message || 'Twenty CRM API error',
details: errorData.details
},
error.response.status || HttpStatus.INTERNAL_SERVER_ERROR
);

Security Headers​

The API uses Helmet middleware for security headers. API documentation routes (/api/docs) use a relaxed CSP to allow Scalar's inline scripts:

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

CORS Configuration​

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

CORS violations result in a browser-side error (no server response body).

Source Files​

FilePurpose
apps/api/src/main.tsGlobal pipes, CORS, Helmet, validation
apps/api/src/api.module.tsGlobal guards and interceptors registration
apps/api/src/logging.interceptor.tsDebug logging interceptor
apps/api/src/config/throttler.config.tsRate limiting configuration