Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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=yourInput Calls the static echo method.
  • GET /User/{id}/greet Calls the instance greet method on the User instance with the specified id.

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 a User by its primary key.
  • POST /User/SAVE?__dataSource= Create or update a User. The User data is passed in the request body as JSON.
  • GET /User/LIST?__dataSource= List all User instances.

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:

TypeDescription
stringString values
numberFloating-point numbers
IntegerInteger values
booleanBoolean values (true/false)
DateDate and time values
Uint8ArrayBinary data
DataSourceOf<T>Any data source for Model type T
unknownJSON data of unknown structure
DeepPartial<T>Partial version of Model type T where anything can be missing
Plain Old ObjectsObjects with properties of supported types
Model typesCustom Models (e.g., User, Post)
ArraysArrays of any supported type (e.g., string[], User[])
Nullable unionsNullable versions of any type (e.g., string | null, User | null)
HttpResult<T>HTTP result wrapping any supported type T
ReadableStreamStream 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);
    }
}