type-client v1.0.44
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
use β an intermediate chain function that accepts a callback with access to the context:
- endpoint.use() π
iServer.endpoint.use(({ ctx }) => { if (!ctx.user) { throw new TypeErr({ code: "UNAUTHORIZED", message: "123" }); } });
input β the function accepts an input schema using zod:
- endpoint.use().input().get()
- endpoint.input().get()
- 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", }; });
get β final chain function with access to input data and context:
- endpoint.use().get()
- endpoint.get() π
iServer.endpoint.get(({ input, ctx }) => { return []; });
post β final chain function with access to input data and context:
- endpoint.use().post()
- 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.
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago