1.0.44 β€’ Published 2 years ago

type-client v1.0.44

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years 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

2 years ago

1.0.25

2 years ago

1.0.29

2 years ago

1.0.28

2 years ago

1.0.27

2 years ago

1.0.33

2 years ago

1.0.32

2 years ago

1.0.31

2 years ago

1.0.30

2 years ago

1.0.37

2 years ago

1.0.36

2 years ago

1.0.35

2 years ago

1.0.34

2 years ago

1.0.39

2 years ago

1.0.38

2 years ago

1.0.40

2 years ago

1.0.44

2 years ago

1.0.43

2 years ago

1.0.42

2 years ago

1.0.41

2 years ago

1.0.19

2 years ago

1.0.18

2 years ago

1.0.17

2 years ago

1.0.16

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.22

2 years ago

1.0.21

2 years ago

1.0.20

2 years ago

1.0.24

2 years ago

1.0.23

2 years ago

1.0.11

2 years ago

1.0.10

2 years ago

1.0.15

2 years ago

1.0.14

2 years ago

1.0.13

2 years ago

1.0.12

2 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.1-alpha.23

3 years ago

1.0.1-alpha.22

3 years ago

1.0.1-alpha.21

3 years ago

1.0.1-alpha.20

3 years ago

1.0.1-alpha.19

3 years ago

1.0.1-alpha.18

3 years ago

1.0.1-alpha.17

3 years ago

1.0.1-alpha.15

3 years ago

1.0.1-alpha.14

3 years ago

1.0.1-alpha.13

3 years ago

1.0.1-alpha.12

3 years ago

1.0.1-alpha.11

3 years ago

1.0.1-alpha.10

3 years ago

1.0.1-alpha.9

3 years ago

1.0.1-alpha.8

3 years ago

1.0.1-alpha.7

3 years ago

1.0.1-alpha.6

3 years ago

1.0.1-alpha.5

3 years ago

1.0.1-alpha.4

3 years ago

1.0.1-alpha.3

3 years ago

1.0.1-alpha.0

3 years ago