1.0.44 β€’ Published 9 months ago

type-client v1.0.44

Weekly downloads
-
License
MIT
Repository
-
Last release
9 months ago

type-server & type-client

Easily create type-safe Backend APIs for nodejs

  • πŸš€Β zero dependencies
  • πŸ“¦Β very light only 35kb (packed 7kb)
  • πŸ’ type-client lib for any frontend
  • only in Typescript ❀️
  • works with Zod ❀️
  • the robust existing API will not change in future releases
  • libraries will always be available

Additional bonuses you will receive:

  • rapid development
  • type-safe development
  • structured endpoints
  • easily scalable application
  • simple and understandable architecture
  • minimum code
  • self-documenting code
  • IDE hints

Installation

install pnpm

For backend πŸ‘‡

pnpm add type-server zod

For client πŸ‘‡

pnpm add type-client

Documentation

Below we will analyze the type-server toolkit using the example of a simple application in the form of a description, pieces of code both with and without an application context.

The file structure of a simple backend application:

  • router/
    • auth.ts
    • admin.ts
  • context.ts
  • db.ts
  • endpoints.ts
  • server.ts

Context 🏁

Creating an application starts with a context, for each request a context will be created in which you can store data and use this data inside endpoints.

backend/context.ts πŸ‘‡

import { inferAsyncReturnType, server } from "type-server";
import { db } from "./db.js";
import { Request } from "@tinyhttp/app";

const createContext = async (req: Request) => {
  const users = await db.getUsers();

  const user = users.find((u) => u.id === 1);

  return {
    user,
    db,
  }; // the returned result is the context
};

type Context = inferAsyncReturnType<typeof createContext>;

export const iServer = server.context<Context>(createContext);

Endpoint πŸͺ

An endpoint is an object that provides a chain of calls to use, get, post

  1. use β†’ an intermediate chain function that accepts a callback with access to the context:

    1. endpoint.use() πŸ‘‡
    iServer.endpoint.use(({ ctx }) => {
      if (!ctx.user) {
        throw new TypeErr({ code: "UNAUTHORIZED", message: "123" });
      }
    });
  2. input β†’ the function accepts an input schema using zod:

    1. endpoint.use().input().get()
    2. endpoint.input().get()
    3. endpoint.input().post() πŸ‘‡
    import { z } from "zod";
    
    iServer.endpoint
      .input(
        z.object({
          email: z.string(),
          password: z.string(),
        })
      )
      .post(async ({ input, ctx }) => {
        console.log(input.email);
        return {
          msg: "Success",
        };
      });
  3. get β†’ final chain function with access to input data and context:

    1. endpoint.use().get()
    2. endpoint.get() πŸ‘‡
    iServer.endpoint.get(({ input, ctx }) => {
      return [];
    });
  4. post β†’ final chain function with access to input data and context:

    1. endpoint.use().post()
    2. endpoint.post() πŸ‘‡
    import { z } from "zod";
    
    iServer.endpoint.post(async ({ input, ctx }) => {
      console.log(input);
      return {
        msg: "Success",
      };
    });

Below is an example of how endpoints can be initialized.

backend/endpoints.ts πŸ‘‡

import { TypeErr } from "type-server";
import { iServer } from "./context.js";

export const publicEndpoint = iServer.endpoint;

export const protectedEndpoint = iServer.endpoint.use(({ ctx }) => {
  if (!ctx.user) {
    throw new TypeErr({ code: "UNAUTHORIZED", message: "123" });
  }
});

In the next Router section, you will see their πŸ‘† application.

Router πŸ—οΈ

A router is the best way to structure endpoints.

  • each router is a group of endpoints united in meaning πŸ‘‡
    export const authRouter = iServer.router({
      signIn: publicEndpoint...,
    	signUp: publicEndpoint...,
    	auth: protectedEndpoint...
    });
  • we can connect routers into a common router with any level of nesting πŸ‘‡

    const authRouter = iServer.router({
      signIn: publicEndpoint...,
    	signUp: publicEndpoint...,
    	auth: protectedEndpoint...
    });
    
    const adminRouter = iServer.router({
      users: protectedEndpoint...,
    });
    
    const router = iServer.router({
      auth: authRouter,
      admin: adminRouter,
    });
  • as a result, routers create a namespace system for easy use and finding the right endpoints πŸ‘‡

    // backend
    const router = iServer.router({
      auth: authRouter,
      admin: adminRouter,
    });
    
    // client
    clientApi.auth.signIn.post();
    clientApi.admin.users.get();
  • using a shared router β€œtype” on the client πŸ‘‡

    // backend
    const router = iServer.router({
      auth: authRouter,
      admin: adminRouter,
    });
    
    export type Router = typeof router;
    
    // client
    import { Router } from "./backend/server.js";
    
    const clientApi = client<Router>("http://localhost:3001");

Above πŸ‘† we considered routers as pieces of code to understand their work, below πŸ‘‡ more real files are presented already in the context of the application.

backend/router/auth.ts πŸ‘‡

import { z } from "zod";
import { iServer } from "../context.js";
import { publicEndpoint } from "../endpoints.js";

export const authRouter = iServer.router({
  signIn: publicEndpoint
    .input(
      z.object({
        email: z.string(),
        password: z.string(),
      })
    )
    .post(async ({ input, ctx }) => {
      return {
        msg: "Success",
      };
    }),
});

backend/router/admin.ts πŸ‘‡

import { iServer } from "../context.js";
import { protectedEndpoint } from "../endpoints.js";

export const adminRouter = iServer.router({
  users: protectedEndpoint.get(async ({ input, ctx }) => {
    const users = await ctx.db.getUsers();
    return users;
  }),
});

Connect to your HTTP Server ⚑

backend/server.ts πŸ‘‡

import { App } from "@tinyhttp/app";
import { cors } from "@tinyhttp/cors";
import bodyParser from "body-parser";
import { createResponse } from "type-server";
import { iServer } from "./context.js";
import { adminRouter } from "./router/admin.js";
import { authRouter } from "./router/auth.js";

const router = iServer.router({
  auth: authRouter,
  admin: adminRouter,
});

export type Router = typeof router;

const app = new App();

app.use(cors());

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.use(async (req, res) => {
  if (req.method === "POST") {
    console.log(req.body);
  }

  const [isOk, result] = await createResponse(
    req.url + "?", // hack: tinyhtto removed marker
    req.method as any,
    req.body,
    req
  );

  console.log(isOk, result);

  if (isOk) {
    return res.status(200).json(result);
  } else if (result) {
    return res.status(400).json(result);
  } else {
    return res.status(500).json({ error: "Critical error" });
  }
});

app.listen(3000);

TypeErr ⚠️

To handle errors, a special class was created that interrupts the execution of the function and returns an error with an object to the client πŸ‘‡

// backend
export const protectedEndpoint = iServer.endpoint.use(({ ctx }) => {
  if (!ctx.user) {
    throw new TypeErr({ code: "UNAUTHORIZED", message: "123" });
  }
});

// client
const signIn = async () => {
  try {
    await clientApi.auth.signIn.post({
      email: "email@email.com",
      password: "123",
    });
  } catch (err) {
    console.log(err); // { code: "UNAUTHORIZED", message: "123" }
  }
};

type-client 🍺

import { client } from "type-client";
import { Router } from "./backend/server.js";

const clientApi = client<Router>(
  "http://localhost:3001" // http server
);

const signIn = async () => {
  try {
    const controller = new AbortController();
    const signal = controller.signal;

    window.setTimeout(() => {
      controller.abort(); // cancel request
    }, 2000);

    await clientApi.auth.signIn.post(
      {
        email: "email@email.com",
        password: "123",
      },
      { signal }
    );
  } catch (err) {
    console.log(err);
  }
};

signIn();

const getUsers = async () => {
  const users = await clientApi.admin.users.get();

  console.log(users);
};

getUsers();

Author

My name is Vladislav Yemelyanov, I am a full stack programmer, although I have experience in many languages, I prefer typescript ❀️ and functional programming, my main specialty is frontend developer, I live and work in Ukraine.

Released under the MIT License.

1.0.26

11 months ago

1.0.25

11 months ago

1.0.29

11 months ago

1.0.28

11 months ago

1.0.27

11 months ago

1.0.33

11 months ago

1.0.32

11 months ago

1.0.31

11 months ago

1.0.30

11 months ago

1.0.37

11 months ago

1.0.36

11 months ago

1.0.35

11 months ago

1.0.34

11 months ago

1.0.39

11 months ago

1.0.38

11 months ago

1.0.40

9 months ago

1.0.44

9 months ago

1.0.43

9 months ago

1.0.42

9 months ago

1.0.41

9 months ago

1.0.19

1 year ago

1.0.18

1 year ago

1.0.17

1 year ago

1.0.16

1 year ago

1.0.9

1 year ago

1.0.8

1 year ago

1.0.7

1 year ago

1.0.6

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.22

1 year ago

1.0.21

1 year ago

1.0.20

1 year ago

1.0.24

1 year ago

1.0.23

1 year ago

1.0.11

1 year ago

1.0.10

1 year ago

1.0.15

1 year ago

1.0.14

1 year ago

1.0.13

1 year ago

1.0.12

1 year ago

1.0.3

1 year ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.1-alpha.23

1 year ago

1.0.1-alpha.22

1 year ago

1.0.1-alpha.21

1 year ago

1.0.1-alpha.20

1 year ago

1.0.1-alpha.19

1 year ago

1.0.1-alpha.18

1 year ago

1.0.1-alpha.17

1 year ago

1.0.1-alpha.15

1 year ago

1.0.1-alpha.14

1 year ago

1.0.1-alpha.13

1 year ago

1.0.1-alpha.12

1 year ago

1.0.1-alpha.11

1 year ago

1.0.1-alpha.10

1 year ago

1.0.1-alpha.9

1 year ago

1.0.1-alpha.8

1 year ago

1.0.1-alpha.7

1 year ago

1.0.1-alpha.6

1 year ago

1.0.1-alpha.5

1 year ago

1.0.1-alpha.4

1 year ago

1.0.1-alpha.3

1 year ago

1.0.1-alpha.0

1 year ago