Model Methods
Models do not define just the source, structure and infrastructure of your data, they also define the API endpoints to operate on that data.
In this section, we will explore how to define methods on Models that are exposed as API endpoints, CRUD methods generated by Cloesce, and the runtime validation that occurs when these methods are invoked.
Static and Instance Methods
A Model class in Cloesce may have both static and instance methods.
A static method is the most simple, it simply exists on the same namespace as the Model class itself. An instance method exists on an actual instance of the Model.
Methods must be decorated with a HTTP Verb such as @GET, @POST, etc. to be exposed as API endpoints.
import { Model, Integer, HttpResult } from "cloesce/backend";
@Model()
export class User {
id: Integer;
name: string;
@GET
static echo(input: string): HttpResult<string> {
if (isBadWord(input)) {
return HttpResult.fail(400, "I'm not saying that!");
}
return HttpResult.ok(`Echo: ${input}`);
}
@GET
greet(): string {
return `Hello, my name is ${this.name}.`;
}
foo() {
// Not exposed as an API method
}
}
After compilation via npx cloesce compile, the above Model will have two API endpoints:
GET /User/echo?input=yourInputCalls the staticechomethod.GET /User/{id}/greetCalls the instancegreetmethod on theUserinstance with the specifiedid.
To query these endpoints, a full generated client that matches the exact structure defined in the Model is available after compilation in .generated/client.ts.
Alpha Note: GET methods currently do not support complex types as parameters (such as other Models, arrays, etc). Only primitive types like
string,number,boolean, etc are supported. This limitation will be lifted in future releases.
CRUD Methods
When creating Models, you will find yourself writing the same CRUD (Create, Read, Update, Delete) boilerplate. To save this effort, Cloesce automatically generates standard CRUD methods if included in the Model decorator. These methods are exposed as API endpoints. Internally, they simply run the Cloesce ORM operations available via the Orm class.
import { Model, Integer } from "cloesce/backend";
@Model(["GET", "SAVE", "LIST"])
export class User {
id: Integer;
name: string;
}
The above User Model will have the following API endpoints generated automatically:
GET /User/{id}/GET?__dataSource=Fetch aUserby its primary key.POST /User/SAVE?__dataSource=Create or update aUser. TheUserdata is passed in the request body as JSON.GET /User/LIST?__dataSource=List allUserinstances.
All CRUD methods take an optional IncludeTree in the request to specify which navigation properties to include in the response. This defaults to no includes.
Note: R2 does not support CRUD methods for streaming the object body, instead it only sends the metadata.
Alpha Note: Delete is not yet supported as a generated CRUD method.
Runtime Validation
When a Model method is invoked via an API call, the Cloesce runtime automatically performs validation on the input parameters and the return value based on what it has extracted from the Model definition during compilation. This ensures that the data being passed to and from the method adheres to the expected types and constraints defined in the Model.
There are many valid types for method parameters in Cloesce, such as:
| Type | Description |
|---|---|
string | String values |
number | Floating-point numbers |
Integer | Integer values |
boolean | Boolean values (true/false) |
Date | Date and time values |
Uint8Array | Binary data |
DataSourceOf<T> | Any data source for Model type T |
unknown | JSON data of unknown structure |
DeepPartial<T> | Partial version of Model type T where anything can be missing |
| Plain Old Objects | Objects with properties of supported types |
| Model types | Custom Models (e.g., User, Post) |
| Arrays | Arrays of any supported type (e.g., string[], User[]) |
| Nullable unions | Nullable versions of any type (e.g., string | null, User | null) |
HttpResult<T> | HTTP result wrapping any supported type T |
ReadableStream | Stream of data |
Plain Old Objects
Cloesce supports the use of Plain Old Objects (POOs) as method parameters and return types. A POO is an object with properties that are of supported types. This allows developers to return or accept complex data structures without needing to define a full Model for them.
import { Model, Integer, HttpResult } from "cloesce/backend";
export class Profile {
bio: string;
age: Integer;
interests: string[];
}
@Model()
export class User {
id: Integer;
name: string;
@POST
updateProfile(profile: Profile): HttpResult<string> {
// Update the user's profile with the provided data
return HttpResult.ok("Profile updated successfully.");
}
}
Note: Plain old objects can only consist of serializable properties supported by Cloesce. They must be exported so that they can be linked. They cannot contain streams.
HttpResult
Every method response in Cloesce is converted to a HttpResult internally. This allows methods to have fine-grained control over the HTTP response, including status codes and headers.
DeepPartial
Cloesce provides a special utility type called DeepPartial<T>, which allows for the creation of objects where all properties of type T are optional, and this optionality is applied recursively to nested objects. This is particularly useful for update operations where you may only want to provide a subset of the properties of a Model.
Stream Input and Output
Cloesce supports streaming data both as input parameters and return values in Model methods. This is particularly useful for handling large files or data streams without loading everything into memory at once. Streams can be hinted to Cloesce using the ReadableStream type.
If a method parameter is of type ReadableStream, no other validation is performed on the input data. Additionally, no other parameters are allowed in the method signature when using a stream input (aside from injected dependencies, which are discussed later).
When a method returns a ReadableStream, Cloesce will return a plain Response on the client side, allowing for efficient streaming of data back to the client.
Cloesce allows a HttpResult<ReadableStream> to be returned as well, which provides the ability to set custom status codes and headers while still streaming data.
For example, to return a R2 object’s body as a stream, you can define a method like this:
import { Model, Integer, R2Object, GET, HttpResult } from "cloesce/backend";
@Model()
export class MediaFile {
id: Integer;
@R2("media/{id}.png", "myBucket")
r2Object: R2Object | undefined;
@GET
downloadFile(): HttpResult<ReadableStream> {
if (!this.r2Object) {
return HttpResult.fail(404, "File not found.");
}
// Body is never loaded into memory, just streamed!
return HttpResult.ok(this.r2Object.body);
}
}