Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 5 additions & 115 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# api-machine - REST API Server Framework
# API-Machine

A lightweight, TypeScript-first REST API framework built on Express with a class-based routing architecture.

Expand Down Expand Up @@ -85,83 +85,17 @@ The [`examples/`](examples/) directory contains comprehensive examples:
- Request body validation
- Structured error responses

## Configuration

### Server Options
#### port `number = 5000`

The port number on which the server will listen for incoming requests.

#### maxPayloadSizeMB `number = 10`

Maximum size in megabytes for JSON request payloads that the server will accept.

#### maxUrlEncodedSizeMB `number = 1`

Maximum size in megabytes for URL-encoded request payloads that the server will accept.

#### log `LogInterface = console`

Custom logger interface for handling server logging (e.g. `ts-tiny-log`). Must implement the LogInterface contract.

#### securityHeaders `SecurityHeadersOptions`

Configuration for HTTP security headers. By default, api-machine is **secure by default** with:
- X-Powered-By header removed (prevents server fingerprinting)
- X-Content-Type-Options: nosniff (prevents MIME sniffing)
- X-Frame-Options: DENY (prevents clickjacking)
- X-XSS-Protection: 1; mode=block (legacy XSS protection)

See **[Security Headers Documentation](docs/security-headers.md)** for detailed configuration options and best practices.

### Example with Options

```typescript
const server = new MyServer({
const server = new MyApiServer({
port: 8080,
maxPayloadSizeMB: 20,
maxUrlEncodedSizeMB: 2,
log: myCustomLogger
});
```

## API Summary

### RestServer

Abstract class for creating REST API servers.

**Methods:**
- `start()`: Starts the server
- `stop()`: Stops the server


### BaseApiRouter

Abstract class for creating route groups.

**Properties:**
- `path`: The base path for the router (e.g., `/api`)

**Methods:**
- `routes()`: Abstract method to define endpoints (must be implemented)

### BaseApiEndpoint

Abstract class for creating API endpoints.

**Properties:**
- `path`: The endpoint path (default: `''`)
- `method`: The HTTP method (default: `GET`). Can be:
- `EndpointMethods.GET`
- `EndpointMethods.POST`
- `EndpointMethods.PATCH`
- `EndpointMethods.PUT`
- `EndpointMethods.DELETE`

**Methods:**
- `handle(request, response, next)`: Abstract method to handle requests (must be implemented)

#### Using Different HTTP Methods

api-machine provides convenience classes for each HTTP method with appropriate default status codes:
Expand Down Expand Up @@ -227,55 +161,11 @@ class DeleteUserEndpoint extends DeleteEndpoint {
}
```

**Available Endpoint Classes:**
- `GetEndpoint` - GET requests (200 OK)
- `PostEndpoint` - POST requests (201 Created)
- `PutEndpoint` - PUT requests (200 OK)
- `PatchEndpoint` - PATCH requests (200 OK)
- `DeleteEndpoint` - DELETE requests (204 No Content)
- `HealthCheckEndpoint` - Pre-built health check endpoint (GET /health)

You can also use `BaseApiEndpoint` and manually set the `method` and `statusCode` properties if needed for custom behavior.

#### Pre-Built Endpoints

##### HealthCheckEndpoint

A ready-to-use health check endpoint that returns system status information. Simply include it in your router:

```typescript
import { BaseApiRouter, HealthCheckEndpoint } from 'api-machine';

class MyRouter extends BaseApiRouter {
override path = '/api';

async routes() {
return [
HealthCheckEndpoint, // Available at GET /api/health
// ... other endpoints
];
}
}
```

**Response Format:**
```json
{
"status": "ok",
"timestamp": "2025-11-08T12:00:00.000Z",
"uptime": 123.45,
"environment": "development"
}
```

**Customizing the Path:**
```typescript
class CustomHealthCheck extends HealthCheckEndpoint {
override path = '/status'; // Available at GET /api/status
}
```

For advanced usage, extending the health check with custom checks, and deployment examples (Kubernetes, Docker, monitoring), see the **[Health Check Endpoint Documentation](docs/health-check-endpoint.md)**.
A ready-to-use health check endpoint that returns system status information. Simply include it in your router. For advanced usage, extending the health check with custom checks, and deployment examples (Kubernetes, Docker, monitoring), see the **[Health Check Endpoint Documentation](docs/health-check-endpoint.md)**.

## Error Handling

Expand Down Expand Up @@ -303,7 +193,7 @@ class GetUserEndpoint extends GetEndpoint {
```

**Key Features:**
- 29 built-in error classes covering HTTP status codes 400-451
- Built-in error classes covering HTTP status codes 400-451
- Automatic JSON error responses with timestamps
- Support for custom headers (e.g., `WWW-Authenticate`, `Retry-After`)
- Optional `details` field for additional context
Expand Down Expand Up @@ -376,7 +266,7 @@ See **[Authentication Documentation](docs/authentication.md)** for complete usag

## Middleware

Routers and endpoints support Express-style middleware for logging, validation, and more. See [Middleware Support](docs/middleware.md) for usage and examples.
Routers and endpoints support Express middleware for logging, validation, and more. See [Middleware Support](docs/middleware.md) for usage and examples.

## Contributing & Development

Expand Down
1 change: 1 addition & 0 deletions examples/complete-example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const server = new MyServer({
port: 3000,
maxPayloadSizeMB: 10,
log: customLogger,
swaggerEnabled: true,
});

server
Expand Down
20 changes: 19 additions & 1 deletion examples/complete-example/users/create-user-endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiRequest, ApiResponse, PostEndpoint } from '../../../src/index';
import { usersRepo, User } from './users-repository';
import { ObjectSanitizer, EmailValidator } from 'valsan';
import { ObjectSanitizer, EmailValidator, IntegerValidator } from 'valsan';
import { NameValSan } from './name-valsan';

/**
Expand All @@ -11,11 +11,29 @@ import { NameValSan } from './name-valsan';
export class CreateUserEndpoint extends PostEndpoint {
override path = '/';

override bodyExample = {
name: 'John Doe',
email: 'john@example.com',
};

override body = new ObjectSanitizer({
name: new NameValSan(),
email: new EmailValidator(),
});

override responseExample = {
id: 3,
name: 'John Doe',
email: 'john@example.com',
created: new Date(),
};

override response = new ObjectSanitizer({
id: new IntegerValidator(),
name: new NameValSan(),
email: new EmailValidator(),
});

async handle(request: ApiRequest, response: ApiResponse) {
const { name, email } = request.body;

Expand Down
15 changes: 15 additions & 0 deletions examples/complete-example/users/get-user-endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ApiRequest, ApiResponse, GetEndpoint } from '../../../src/index';
import { NotFoundError } from '../../../src/error';
import { usersRepo } from './users-repository';
import { ObjectSanitizer, IntegerValidator, EmailValidator } from 'valsan';
import { NameValSan } from './name-valsan';

/**
* Complete Example - Get User Endpoint
Expand All @@ -11,6 +13,19 @@ import { usersRepo } from './users-repository';
export class GetUserEndpoint extends GetEndpoint {
override path = '/:id';

override responseExample = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
created: new Date('2023-01-01'),
};

override response = new ObjectSanitizer({
id: new IntegerValidator(),
name: new NameValSan(),
email: new EmailValidator(),
});

async handle(request: ApiRequest, response: ApiResponse) {
const userId = parseInt(request.params['id'], 10);
const user = usersRepo[userId];
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api-machine",
"version": "1.0.1",
"version": "1.1.0",
"description": "api-machine",
"private": "true",
"typescript-template": {
Expand Down
10 changes: 10 additions & 0 deletions src/oas/oas-endpoint-component-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export class OasEndpointComponentConverter {
});
}

const responseSanitizer = endpoint.response;

if (responseSanitizer) {
this.addSchema({
name: `${endpoint.name}Response`,
sanitizer: responseSanitizer,
example: endpoint.responseExample,
});
}

return this.schemas;
}

Expand Down
26 changes: 23 additions & 3 deletions src/oas/oas-endpoint-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
PathItemObject,
ParameterObject,
RequestBodyObject,
ResponsesObject,
} from 'auto-oas/oas/v3.1';

import { BaseApiEndpoint } from '../router';
Expand Down Expand Up @@ -60,9 +61,28 @@ export class OasEndpointConverter {
// Use statusCode for response
const status = endpoint.statusCode;

const responses = {
[status]: { description: 'Success' },
};
const responses: ResponsesObject = {};

// Generate success response with schema if available
const responseSanitizer = endpoint.response;
if (responseSanitizer && responseSanitizer.schema) {
responses[status] = {
description: 'Success',
content: {
'application/json': {
schema: {
$ref:
'#/components/schemas/' +
endpoint.getName() +
'Response',
},
},
},
};
}
else {
responses[status] = { description: 'Success' };
}

const errors = endpoint.getErrors();
for (const error in errors) {
Expand Down
4 changes: 4 additions & 0 deletions src/router/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export abstract class BaseApiEndpoint extends BaseApiRoute {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public headersExample?: any;

public response?: ObjectSanitizer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public responseExample?: any;

public getErrors(): { [key: string]: HTTPError } {
return {
parse: new BadRequestError(),
Expand Down
24 changes: 21 additions & 3 deletions src/router/endpoints/health-check-endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { ApiRequest, ApiResponse } from '../endpoint';
import {
Iso8601TimestampValSan,
MinLengthValidator,
ObjectSanitizer,
StringToNumberValSan,
} from 'valsan';
import { GetEndpoint } from './get-endpoint';

export class HealthCheckEndpoint extends GetEndpoint {
override path = '/health';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async handle(_request: ApiRequest, _response: ApiResponse) {
override responseExample = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: 12345,
environment: 'development',
};

override response = new ObjectSanitizer({
status: new MinLengthValidator(),
timestamp: new Iso8601TimestampValSan(),
uptime: new StringToNumberValSan(),
environment: new MinLengthValidator(),
});

async handle() {
return {
status: await this.getStatus(),
timestamp: await this.getTimestamp(),
Expand Down
6 changes: 5 additions & 1 deletion src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export abstract class BaseApiRouter extends BaseApiRoute {
(instance as BaseApiEndpoint).tag = tag;
}

await instance.register(this.router, this.fullPath);
await this.registerInstance(instance);

this.registeredRoutes.push(instance);

Expand Down Expand Up @@ -83,6 +83,10 @@ export abstract class BaseApiRouter extends BaseApiRoute {
});
});
}

protected async registerInstance(instance: BaseApiRoute): Promise<void> {
await instance.register(this.router, this.fullPath);
}
}

export type ApiRouter = { new (): BaseApiRouter };
Loading