Skip to content

HTTP

Custom HTTP layer built on Deepkit's HTTP module with an enhanced kernel, middleware system, error handling, CORS, file uploads, and request-scoped caching.

Custom HTTP Kernel

CustomHttpKernel replaces Deepkit's default kernel with:

  • Configurable request logging (mode set via HTTP_REQUEST_LOGGING_MODE)
  • Request duration tracking
  • Active request counting
  • AsyncContext integration (request ID, trace ID)
  • Abort detection for incomplete requests

Logging Modes

ModeDescription
e2eLog at request start and end
finishLog only at request end
errorsLog only errors
noneNo request logging

Set via HTTP_REQUEST_LOGGING_MODE environment variable. Health check logging can be disabled separately with HEALTHZ_ENABLE_REQUEST_LOGGING=false.

Middleware

Extend HttpMiddleware for middleware that properly handles HTTP errors and injects CORS headers into error responses:

typescript
import { HttpMiddleware } from '@zyno-io/dk-server-foundation';

class RateLimitMiddleware extends HttpMiddleware {
    async handle(request: HttpRequest, response: HttpResponse) {
        const ip = request.getRemoteAddress();
        if (await isRateLimited(ip)) {
            throw new HttpError(429, 'Too many requests');
        }
    }
}

Deepkit's default middleware returns 404 on errors -- this base class fixes that behavior.

Debug Middleware

HttpLogPayloadMiddleware logs request method, URL, content-type, and body for debugging.

CORS

Multi-origin CORS support with regex matching.

Single Origin

typescript
const app = createApp({
    cors: config => ({
        hosts: ['https://myapp.com'],
        credentials: true
    })
});

Multiple Origins

typescript
const app = createApp({
    cors: config => [
        {
            hosts: ['https://app.example.com', /https:\/\/.*\.example\.com/],
            credentials: true
        },
        {
            hosts: ['https://api.partner.com'],
            paths: [/^\/api\/partner/],
            methods: ['GET', 'POST']
        }
    ]
});

HttpCorsOptions

PropertyTypeDescription
hosts(string | RegExp)[]Allowed origins
paths(string | RegExp)[]Restrict CORS to specific paths
methodsstring[]Allowed HTTP methods
credentialsbooleanAllow credentials
allowHeadersstring[]Allowed request headers
exposeHeadersstring[]Exposed response headers

CORS headers are precomputed early in the request workflow and injected into responses, including error responses from middleware.

Error Handling

Built-in HTTP Errors

typescript
import { HttpUserError, HttpDetailedAccessDeniedError } from '@zyno-io/dk-server-foundation';

// 422 - User error
throw new HttpUserError('Invalid email format');

// 403 - Access denied with custom message
throw new HttpDetailedAccessDeniedError('Insufficient permissions for this resource');

Error Response Format

All errors are returned as JSON:

json
{ "error": "Error message here" }

The workflow listener standardizes error handling:

  • Validation errors in route parameters -> HTTP 400
  • JSON parse errors -> HTTP 400
  • Validation errors in controllers -> HTTP 500 (internal error)
  • Non-HTTP errors are decorated with request body context for debugging (when enabled)

Request Body Logging

By default, the workflow listener attaches the request body to non-HTTP errors for debugging. This is controlled by HttpWorkflowListenerOptions:

typescript
import { HttpWorkflowListenerOptions } from '@zyno-io/dk-server-foundation';
PropertyTypeDefault
logRequestBodyOnErrorbooleantrue in development, false in production

When NODE_ENV=production, request body logging is disabled to avoid leaking sensitive data. Override by providing HttpWorkflowListenerOptions as a DI provider:

typescript
const app = createApp({
    providers: [{ provide: HttpWorkflowListenerOptions, useValue: Object.assign(new HttpWorkflowListenerOptions(), { logRequestBodyOnError: true }) }]
});

Response Type Helpers

The library exports type aliases used as return type annotations on controller methods:

typescript
import { OkResponse, RedirectResponse, EmptyResponse, AnyResponse } from '@zyno-io/dk-server-foundation';

class MyController {
    // Return { ok: true }
    @http.POST('/action')
    async doAction(): OkResponse {
        // ... perform action ...
        return OkResponse; // the exported const { ok: true }
    }

    // Redirect (return type annotation)
    @http.GET('/old-path')
    async redirect(): RedirectResponse {
        // Use Deepkit's redirect mechanism
    }

    // Empty response (return type annotation)
    @http.DELETE('/resource/:id')
    async delete(): EmptyResponse {
        // ...
    }

    // Any response (bypasses serialization)
    @http.GET('/raw')
    async raw(): AnyResponse {
        // ...
    }
}

OkResponse is also exported as a const value { ok: true } for convenience.

File Uploads

typescript
import { FileUpload } from '@zyno-io/dk-server-foundation';

class UploadController {
    @http.POST('/upload')
    async upload(file: FileUpload) {
        console.log(file.path); // Temporary file path
        console.log(file.size); // Size in bytes
        console.log(file.type); // MIME type
        console.log(file.originalName); // Original filename
    }
}

Multipart forms use _payload as the JSON key for non-file fields.

Request-Scoped Caching

Cache expensive computations per request to avoid redundant database lookups:

typescript
import { getOrCacheValue, getCachedValue } from '@zyno-io/dk-server-foundation';

const USER_KEY = Symbol('user');

// Compute and cache
const user = await getOrCacheValue(request, USER_KEY, async () => {
    return await db.query(User).filter({ id: userId }).findOne();
});

// Retrieve cached value later in the same request
const cachedUser = getCachedValue(request, USER_KEY);

Parameter Resolvers

Create Deepkit route parameter resolvers that cache their results:

typescript
import { createCachingParameterResolver } from '@zyno-io/dk-server-foundation';

const CurrentUserResolver = createCachingParameterResolver(USER_KEY, async context => {
    // context is RouteParameterResolverContext (has .request, .type, etc.)
    const jwt = await getJwtFromRequest(context.request);
    return await db.query(User).filter({ id: jwt.subject }).findOne();
});

Real IP Support

When behind a reverse proxy, enable x-real-ip header support:

bash
USE_REAL_IP_HEADER=true

This overrides HttpRequest.getRemoteAddress() to return the value from the x-real-ip header.

HTTP Context

The HTTP context provides request-scoped data that flows through AsyncContext:

typescript
import { setHttpContextResolver } from '@zyno-io/dk-server-foundation';

// Override the default context provider (which generates a reqId)
setHttpContextResolver(request => ({
    reqId: generateRequestId(),
    tenantId: request.headers['x-tenant-id']
}));

Released under the MIT License.