Stainless helps you ship quality, typesafe REST APIs from any TypeScript backend.
You declare the shape and behavior of your API in one place, and get an OpenAPI spec, docs, and typed frontend client without a build step.
You can use it as a pluggable, batteries-included web framework for APIs (managing auth, pagination, observability, etc) or sprinkle it on top of your existing API in any framework for better OpenAPI support and/or full-stack typesafety.
You can also opt into Stainless's Stripe-inspired pristine API design conventions and get rich pagination, consistent errors, field inclusion & selection, and (WIP) normalized caching on the frontend for free.
Stainless draws inspiration with gratitude from tRPC, FastAPI, GraphQL/Relay, and (heavily) from the internal API Framework we worked on at Stripe.
Note This project is currently in its alpha stage of development. Features may be changed or removed at any time without warning, and production use is not recommended. Hobbyists welcome!
For example:
// server.ts
import { Stl } from "stainless";
import { makePrismaPlugin } from "@stl-api/prisma";
import { makeNextPlugin } from "@stl-api/next";
import { makeNextAuthPlugin } from "@stl-api/next-auth";
import { authOptions } from "~/pages/api/auth/[...nextauth]";
import prisma from "~/libs/prisma";
import { z } from "stainless";
const plugins = {
next: makeNextPlugin(),
nextAuth: makeNextAuthPlugin({ authOptions }),
prisma: makePrismaPlugin(),
};
export const stl = new Stl({
plugins,
});
const User = z.object({
id: z.string(),
name: z.string(),
});
const update = stl.endpoint({
endpoint: "POST /users/:id",
description: "Update a user. Currently only updating the name is supported.",
authenticated: true,
response: User,
path: z.object({
id: z.string(),
}),
body: z.object({
name: z.string(),
}),
async handler({ id, name }) {
return await prisma.users.updateOne(id, { name });
},
});
export const api = stl.api({
openapi: {
endpoint: "GET /api/openapi",
},
resources: {
users: stl.resource({
actions: {
update,
},
models: {
User,
},
}),
},
});
// client.ts
import { createClient } from "stainless";
import type { api } from "./server";
const client = createClient<typeof api>("http://localhost:3000/api");
// params are fully typed:
const user = await client.users.update("id", { name: "my new name" });
// user object is fully typed.
// A full OpenAPI spec is available by default at "get /openapi":
const openapi = await client.getOpenapi();
console.log(openapi.paths["/users/:id"].post);
console.log(openapi.components.schemas.User);
See stainless
package docs to get started!
Pristine is an API Standard by Stainless, providing opinions on API design so teams don't have to bikeshed, and so tools can expect consistent API shapes.
Following the Pristine Standard helps your API offer an interface like Stripe's, with best-practices baked in. Like the Relay standard for GQL, Pristine can also help tooling like frontend clients cache data, paginate requests, handle errors, and so on.
Here is a list of Pristine API design conventions:
If you'd like a maintainable way of declaring your OpenAPI spec
in TypeScript, right alongside your application code, and getting
great docs, end-to-end typesafety, and backend API client libraries (SDKs),
you can adopt the stainless
library gradually in minimally-invasive ways.
For example, in an Express app, you can add annotations near a handler to get an OpenAPI spec and client:
// app/routes/users.ts
const User = z.object({
id: z.string(),
name: z.string(),
});
const create = stl.endpoint({
endpoint: "POST /users",
response: User,
body: z.object({ name: z.string() }),
});
app.post("/users", async (req, rsp) => {
const user = await db.users.create({ name });
rsp.status(200).json(user);
});
// app/api.ts
const users = stl.resource({
models: { User },
actions: {
create,
},
});
const api = stl.api({
resources: {
users,
},
});
// and voila, you get an OpenAPI spec!
app.get("/openapi", (req, rsp) => {
rsp.json(api.openapi.spec);
});
app.post("/users", async (req, rsp) => {
const { name } = create.validateParams(req);
const user = await db.users.create({ name });
rsp.send(create.makeResponse(user));
});
Doing this helps TypeScript ensure that your OpenAPI spec is an accurate reflection of your runtime behavior. It can also help return consistent response shapes and validation error messages to the user.
Note that validateParams
raises BadRequestError
if params don't match.
To handle errors like this, add middleware:
app.use((err, req, rsp, next) => {
if (err instanceof stl.Error) {
rsp.status(err.statusCode).send(stl.makeError(err));
}
// …
});
stl.makeError
is will return a JSON object with type
, message
, and other information. (TODO add/encourage things like request ID's…)