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.
stainless
: Stainless server and client framework- Table of Contents
- Ecosystem
- Getting Started
- In-depth topics
Stainless provides plugins for integrating with the following tools. We plan to add more in the future!
- Next.js:
@stl-api/next
- NextAuth.js:
@stl-api/next-auth
- Prisma:
@stl-api/prisma
Warning
This is alpha software, and we may make significant changes in the coming months. We're eager for you to try it out and let us know what you think!
At the moment, Stainless can be used with Next.js. Support for
standalone and Express apps is coming soon.
We will soon provide a create-stl-app
API. Until then:
npm i --save stainless-api/stl-api#stainless-0.0.3 stainless-api/stl-api#next-0.0.3
# Optional plugins:
npm i --save stainless-api/stl-api#next-auth-0.0.3 # If you are using next-auth
npm i --save stainless-api/stl-api#prisma-0.0.3 # If you are using Prisma
// ~/libs/stl.ts
import { Stl } from "stainless";
import { makeNextPlugin } from "@stl-api/next";
export type Context = {};
const plugins = {
next: makeNextPlugin(),
};
export const stl = new Stl({
plugins,
});
// ~/api/users/models.ts
import { z } from "stainless";
import prisma from "~/libs/prisma";
export const User = z
.response({
id: z.string().uuid(),
name: z.string().nullable().optional(),
username: z.string().nullable().optional(),
bio: z.string().nullable().optional(),
email: z.string().nullable().optional(),
emailVerified: z.date().nullable().optional(),
image: z.string().nullable().optional(),
coverImage: z.string().nullable().optional(),
profileImage: z.string().nullable().optional(),
hashedPassword: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
followingIds: z.array(z.string().uuid()),
hasNotification: z.boolean().nullable().optional(),
followersCount: z.number().optional(),
})
.prismaModel(prisma.user);
// ~/api/users/retrieve.ts
import { stl } from "~/libs/stl";
import { NotFoundError, z } from "stainless";
import prisma from "~/libs/prismadb";
import { User } from "./models";
export const retrieve = stl.endpoint({
endpoint: "GET /api/users/{userId}",
response: User,
path: z.object({
userId: z.string(),
}),
async handler({ userId }, ctx) {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!user) throw new NotFoundError();
return user;
},
});
// ~/api/users/index.ts
import { stl } from "~/libs/stl";
import { retrieve } from "./retrieve";
import { User } from "./models";
export const users = stl.resource({
summary: "Users",
models: {
User,
},
actions: {
retrieve,
},
});
// ~/api/index.ts
import { stl } from "~/libs/stl";
import { users } from "./users";
export const api = stl.api({
openapi: {
endpoint: "GET /api/openapi",
},
resources: {
users,
},
});
Warning
Currently the names of
resources
have to match the URL paths for the client to work. For example if the base URL is/api
and there is aGET /api/users
endpoint, the resource must be namedusers
here. If it were nameduser
, thenclient.user.list(...)
wouldGET /api/user
, the wrong URL. We plan to make a build watch process to compile a list of endpoint URLs for the client to remove this limitation.
// ~/app/api/[...catchall]/route.ts
import { api } from "~/api/index";
import { stl } from "~/libs/stl";
const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } =
stlNextAppCatchAllRouter(api, {
catchAllParam: "catchall",
});
export { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS };
// ~/api/client.ts
import { createClient } from "stainless";
import type { api } from "./index";
export const client = createClient<typeof api>("/api");
// ~/app/users/[userId]/page.tsx
import * as React from "react";
import client from "~/api/client.ts";
import { useQuery } from "@tanstack/react-query";
export default function UserPage({
params: { userId },
}: {
params: { userId: string };
}): React.ReactElement {
const { status, error, data: user } = useQuery({
queryKey: [`users/${userId}`],
queryFn: () => client.users.retrieve(userId),
});
if (status === "loading") return <LoadingAlert>Loading user...</LoadingAlert>;
if (error) return <ErrorAlert error={error} />;
return <UserDetailsPanel user={user}>
}
Note We may provide a plugin that adds
client.users.useRetrieve
hooks in the future.
Stainless provides helpers for easily implementing pagination that follows the pristine convention, and makes it easy to implement pagination with Prisma.
Inclusion allows you to optionally include associated objects in an API response if the
user requests them in an include
query parameter. Stainless makes it easy to implement
inclusion with Prisma.
Selection allows you to pick what subset of fields on an associated object are returned
in an API response, if the user requests them in a select
query parameter. Stainless
makes it easy to implement selection with Prisma.