Compas

Unified backend tooling

Flexible code generators

Code generate routers, validators, api clients and Postgres queries.

ES Modules first

Contains test, bench and script runner supporting only ES modules.

Common utilities

Comes with session handling, job queue, file storage, script runner and more.

I'm a...

Backend developer

Provide the HTTP api structure:

const T = new TypeCreator("todo");
const R = T.router("/todo");

R.get("/list", "list").response({
  todos: [
    {
      id: T.uuid(),
      todo: T.string(),
      completed: T.boolean(),
    },
  ],
});

Add an implementation:

todoHandlers.list = async (ctx, next) => {
  ctx.body = {
    todo: [
      {
        id: uuid(),
        todo: "Explore compas",
      },
    ],
  };

  return next();
};

Create a test:

test("todo/controller", async (t) => {
  const apiClient = Axios.create({});
  await createTestAppAndClient(app, apiClient);

  t.test("list conforms to response structure", async (t) => {
    await todoApi.list();
    // Throws: validator.response.todo.list.boolean.type -> Missing boolean value at '$.todo[0].completed'
  });
});

And some icing on the cake, by generating some PostgreSQL queries:

const T = new TypeCreator("database");

T.object("user")
  .keys({
    email: T.string().searchable(),
    name: T.string(),
  })
  .enableQueries({
    withDates: true,
  })
  .relations(T.oneToMany("posts", T.reference("database", "post")));

T.object("post")
  .keys({
    title: T.string(),
    body: T.string(),
  })
  .enableQueries({
    withSoftDeletes: true,
  })
  .relations(T.manyToOne("author", T.reference("database", "user"), "posts"));

With queries like the following:

const [user] = await queryUser({ where: { email: "foo@bar.com" } }).exec(sql);
const usersWithPosts = await queryUser({ posts: {} }).exec(sql);

const postsForAuthor = await queryPost({ where: { author: user.id } }).exec(
  sql,
);
const [authorOfPost] = await queryUser({
  viaPosts: { where: { id: postsForAuthor[0].id } },
}).exec(sql);
// postsForAuthor[0].author == authorOfPost.id

await queries.userInsert(sql, { email: "bar@foo.com", name: "Compas " });

// soft delete
await queries.postUpdate(sql, {
  update: {
    deletedAt: new Date(),
  },
  where: { id: "c532ac2a-4489-4b50-a061-12b2aa9a5df2" },
});
// Search include soft deleted posts
await queryPost({ where: { deletedAtIncludeNotNull: true } });
// permanent delete
await queries.postDelete(sql, {
  id: "c532ac2a-4489-4b50-a061-12b2aa9a5df2",
});

Frontend developer

Either import from a Compas server:

const app = new App();
app.extend(
  await loadApiStructureFromRemote(Axios, "https://api.my-domain.com"),
);
app.generate({
  outputDirectory: "./src/generated",
  isBrowser: true,
});

Or import from an OpenAPI spec (alpha-quality :S):

const app = new App();
app.extendWithOpenApi("todo", getAnOpenAPISpecAsPlainJavascriptObject());
app.generate({
  outputDirectory: "./src/generated",
  isBrowser: true,
});

And use the typed api client:

const todos: TodoListResponse = await apiTodoList();

Or use the generated react-query hooks:

function renderTodo({ todoId }: TodoSingleParams) {
  // Generated react-query hook with typed results
  const { data } = useTodoSingle({ todoId });

  return <div>{/*...*/}</div>;
}

How it works

Most of the above is achieved by a custom specification, a few code generators and a bunch of time tweaking results to achieve a stable way of working. Which benefits backend developers with less copy-pasting, easy 'interface'-testing and less manual doc writing, and frontend developers with explorable and ready to consume api's.

Why

My work involved doing many small projects. I had a hard time backporting incremental fixes to existing projects. To facilitate my needs more and to stop copying and pasting things around, this project was born.

New features

New features added should fall under the following categories:

  • It improves the interface between api and client in some way. An example may be to support websockets in @compas/code-gen
  • It improves the developer experience one way or another while developing an api For example the compas docker commands or various utilities provided by @compas/stdlib

Although some parts heavily rely on conventions set by the packages, we currently aim not to be a framework. We aim to provide a good developer experience, useful abstractions around the basics, and a stable backend <-> client interface.

MIT Licensed | Copyright © 2019-present Dirk de Visser