Introduction

What is @httpc/kit?

@httpc/kit is a highly opinionated framework for building function-based API with minimal boilerplate code and e2e type safety.


@httpc/kit is built on top of @httpc/server and provides many ready-to-go components covering concerns like authentication, validation, caching and so on…


@httpc/kit major goals are:

Writing less code: minimizing repetition, redundancy and boilerplate

Functions at the core

In httpc, functions are first class citizen. You can expose a function with just its definition. No scaffolding, no binding, no parsing. Nothing extra is required.


You expose just plain functions.

function greet(name: string) {
    return `Hello ${name}`;
}

async function loadPosts() {
    return await db.select("posts").take(10);
}

export default {
    greet,
    loadPosts,
}

You can build a pipeline around a function with a simple middleware sequence.

import { httpCall } from "@httpc/kit";

const approveTicket = httpCall(
    Authenticated("role:admin"),   // <-- authenticated with required role
    Validate(Number, String),      // <-- validate arguments
    async (ticketId: number, message: string) => {
        /** function code */
    }
)

export default {
    approveTicket
}

You can also apply the same middlewares to a group of functions.

import { httpGroup } from "@httpc/kit";

function join(room: string) { /** */ }
function send(message: string) { /** */ }
function leave() { /** */ }

export default httpGroup(
    Authenticated(),
    Session(),
    {
        join,
        send,
        leave
    }
)

Simplified API layout

Functions can be composed and arranged like plain objects. No custom dsl, external code or unnatural syntax.


Define the API structure with just out-of-the box javascript esm exports.

function getPost(id: string) { /**  */ }
function updatePost(id: string, data: Post) { /**  */ }
function deletePost(id: string) { /**  */ }

export default {
    posts: {
        get: getPost,
        update: updatePost,
        delete: deletePost,
    }
}

You can aggregate and import functions from different files and create an API layout easier to access and use.

import posts from "./calls/posts";
import comments from "./calls/comments";

export default {
    posts,
    comments,
}

Because function calls are just plain objects, you can extend, merge and nest them at will.


The API structure can be defined with all the constructs javascript provides.

import posts from "./calls/products";
import comments from "./calls/comments";

function login(username: string, password) { /** */}
function logout() { /** */}

function addReaction(postId: string, reaction: string) { /** .. */}
function removeReaction(id: string) { /** .. */}

export default {
    posts: {
        ...posts, // <-- merge all posts calls
        reactions: { // <-- nesting sub calls
            add: addReaction,
            remove: removeReaction
        }
    },
    comments,
    login, // <-- exposed as first level
    logout
}

Context always available

Thanks to execution context propagation, the the request context follows the request processing even in case of asynchronous invocations like db queries or external api calls.


You can get the context from anywhere.

function trace(message: string) {
    const { requestId } = useContext();
}

With the context always at reach, there’s no need to pass parameters around and bloat function arguments with context information.


Context can be extended and defined to suit your need.


Context information, can be read and set with the useContextProperty hook.

// read the property
// this is an alternative syntax for the destructuring { ... } = useContext();
const session = useContextProperty("session");

// write the property
useContextProperty("session", session);

Context hooks

Context hooks, or simply hooks, provides predefined behaviors around the request context.

function getPrivateArea() {
    const user = useUser();
    // other code
}

In the above example, the useUser hook returns the user associated with request if authenticated. Otherwise, it will raise an UnauthorizedError.


@httpc/kit provides many builtin hooks that cover common operations, like authentication, permissions management, value caching, service resolution…

Per convention, a hook name starts with use. Usually a hook can both read and write to the context, with the following pattern:

// read request authorizations
let authorizations = useAuthorizations();

authorization = authorizations.mergeWith("role:admin");

// write the authorizations
useAuthorizations(authorizations);

Some hooks require one or more parameters to read from the context. In that case, the write happens when an extra optional parameter is used. For example:

const session = useContextProperty("session");

// write the property
useContextProperty("session", session);

Each hook has a specific behavior but, in general, the last parameter works as the set value.

Pursuing extensive type safety

Writing safer code is httpc a major goal. Because javascript is a dynamic language, safer code is the code that gets checked while developing or during the build step.


@httpc/kit goes to a great length to provide safety, with a focus on the following areas:

End-to-end type safe functions calls

End-to-end (e2e) type safety involves techniques to keep the API schema in sync with the client code calling it. In other words, any API change impacts the client at compile time: when the API definition updates, the client code breaks if something incompatible arises.


With e2e type safety, clients benefit a more trustworthy environment because the code is checked beforehand during the development or build phase, reducing crashes while the code runs in production.


httpc provides two ways to get e2e type safety:

Definition extensions

You can define custom context properties. Extending context definition allows keeping your code safe. The typescript compiler will type check your code and emit errors in case of type mismatch.


To enrich the context with new properties, just create a file called env.d.ts in your project:

/// <reference types="@httpc/kit/env" />

global {
    interface IHttpCContext {
        // example custom property
        environment: string

        // other custom properties here
        // ...
    }
}

Now you can access the property from the context as usual with the useContext hook:

const { environment }  = useContext();

Service definitions

Service types are another example of definition extensions.


When you inject or resolve a service, to keep code safe you need to know its type. But sometimes, the type is not available from the context.


Traditionally this is accomplished with a manual cast. But casting is error prone, because you need to remember the concrete type every time the service is used. In addition, if after a refactor the service changes label, the resolution will fail at run time.


In httpc, you can define new services with the relative types, either an interface or a class.

/// <reference types="@httpc/kit/env" />
import { PaymentProvider } from "./services";

global {
    interface ServiceTypes {
        OnlinePayments: PaymentProvider
        WirePayments: PaymentProvider
    }
}

Now you can resolve both payment services, with full type safe code, autocompletion support and no casting needed.

function makePayment(data: PaymentData) {
    const payments = useInjected("OnlinePayments"); // <-- "OnlinePayments" is suggested
    
    // payments here is fully typed
    payments.make(/**  */);
    payments.commit(/**  */);
}

For details and all extension points read Extending.

Magic strings reduction

A magic strings is a static, predefined text value often used as identifier or a comparison value. Magic strings brings many caveats, but for some use cases they are the only choice.


@httpc/kit helps to keep them as minimal as possible.


For example for environment variables, you can explicitly list them in a central location:

/// <reference types="@httpc/kit/env" />

global {
    interface IEnvVariables {
        LOG_LEVEL: string
    }
}

With the above definitions, you’ll get editor autocompletion when you resolve an environment variable.

class Logger {
    constructor (
        @env("LOG_LEVEL") logLevel: string
    ) {
    }
}

In the above example, the editor will autosuggest LOG_LEVEL every time an environment variable is used with no need to constantly check the name or see the list of the available ones.


Within @httpc/kit, strings as keys are used in other places like cache resolution. To know where magic-strings are used and can be redefined read [Extending](/docs/kit-extending).

Toolkit ready to use

@httpc/kit provides many builtin components and utilities covering many use cases. Checkout the dedicated pages:

Dependency injection

@httpc/kit leverages the tsyringe package to use dependency injection. With tsyringe you can create containers and register services, singletons, factories with a nice typed constructor parameter resolution.

import { singleton } from "tsyringe";
import { DatabaseService, LogService } from "./services";

@singleton()
export class UserService {
    constructor(
        private db: DatabaseService,
        private logger: LogService,
    ) {

    }
}

Tsyringe is a required peer dependency. You can find more details about tsyringe on its repository.


@httpc/kit provides additional helpers to deal better with injection or to cover use cases that tsyringe lacks. For example: optional parameters or resolving environment variables. Details on the Services & Dependency page.


You can find many examples on how to use dependency injection in the tutorials.

Request context

@httpc/kit extends the the builtin properties inherited from the server context with additional properties.

propertytypedescription
containerDependencyContainerthe service container scoped to the request
userIUser?the user identity if the request is authenticated
authorizationAuthorization?the permissions the request is granted

In addition, @httpc/kit offers several hooks to interact with context.

import { useUser } from "@httpc/kit";

async function getMyArticles() {
    const user = useUser(); // throw Unauthorized if request is not authenticated
    
    return await db.select("articles").where("userId", user.id);
)