Skip to content

Generate standard Protobuf and ts-rest APIs following best-practice design patterns

License

Notifications You must be signed in to change notification settings

thames-technology/apigen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

API Gen Logo

If you use this repo, star it ✨


Generate standard Protobuf and ts-rest APIs following best-practice design patterns

Inspired by API Design Patterns


Install

Homebrew:

brew install thames-technology/tap/apigen

Go:

go install github.com/thames-technology/apigen

Getting started

Protobuf

Create BookService with author parent resource:

apigen proto -r book -p author -pkg bookservice.v1alpha1 -w

This will generate the following standard API definitions in proto/bookservice/v1alpha1/service.proto:

syntax = "proto3";

package bookservice.v1alpha1;

import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";

// =============================================================================
// Service definition
// =============================================================================

// The BookService defines the standard methods for managing books.
service BookService {
  // Creates a new book in the library.
  rpc CreateBook(CreateBookRequest) returns (CreateBookResponse);
  // Retrieves a book by its ID.
  rpc GetBook(GetBookRequest) returns (GetBookResponse);
  // Lists books with pagination support.
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);
  // Updates an existing book.
  rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse);
  // Deletes a book by its ID.
  rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty);
}

// =============================================================================
// Service request and response messages
// =============================================================================

// Request message for CreateBook method.
message CreateBookRequest {
  // UUID used to prevent duplicate requests.
  optional string request_id = 1;
  // The book to be created.
  Book book = 2;
}

// Response message for CreateBook method.
message CreateBookResponse {
  // The created book.
  Book book = 1;
}

// Request message for GetBook method.
message GetBookRequest {
  // ID of the book to retrieve.
  string book_id = 1;
}

// Response message for GetBook method.
message GetBookResponse {
  // The retrieved book.
  Book book = 1;
}

// Request message for ListBooks method.
message ListBooksRequest {
  // The maximum number of books to return.
  optional int32 page_size = 1;
  // The token to retrieve the next page of results.
  optional string page_token = 2;
  // ID of the author to list books for.
  optional string author_id = 3;
}

// Response message for ListBooks method.
message ListBooksResponse {
  // The list of books.
  repeated Book results = 1;
  // Token to retrieve the next page of results.
  string next_page_token = 2;
  // Estimated total number of results.
  int32 total_results_estimate = 3;
}

// Request message for UpdateBook method.
message UpdateBookRequest {
  // UUID used to prevent duplicate requests.
  optional string request_id = 1;
  // ID of the book to update.
  string book_id = 2;
  // The book to be updated.
  Book book = 3;
  // The field mask specifying the fields to update.
  google.protobuf.FieldMask update_mask = 4;
}

// Response message for UpdateBook method.
message UpdateBookResponse {
  // The updated book.
  Book book = 1;
}

// Request message for DeleteBook method.
message DeleteBookRequest {
  // ID of the book to delete.
  string id = 1;
}

// =============================================================================
// Resource messages
// =============================================================================

// The Book message represents a book in the library.
message Book {
  // Unique identifier for the book.
  string id = 1;
  // When the book was created.
  google.protobuf.Timestamp create_time = 2;
  // When the book was last updated.
  google.protobuf.Timestamp update_time = 3;
  // ID of the author of the book.
  string author_id = 4;
  // Additional fields for the book.
  // TODO: Add fields here.
}

Create AuthorService definition without a parent resource:

apigen proto -r author -pkg authorservice.v1alpha1 -w

This will generate the following standard API definitions in proto/authorservice/v1alpha1/service.proto:

syntax = "proto3";

package authorservice.v1alpha1;

import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";

// =============================================================================
// Service definition
// =============================================================================

// The AuthorService defines the standard methods for managing authors.
service AuthorService {
  // Creates a new author in the library.
  rpc CreateAuthor(CreateAuthorRequest) returns (CreateAuthorResponse);
  // Retrieves a author by its ID.
  rpc GetAuthor(GetAuthorRequest) returns (GetAuthorResponse);
  // Lists authors with pagination support.
  rpc ListAuthors(ListAuthorsRequest) returns (ListAuthorsResponse);
  // Updates an existing author.
  rpc UpdateAuthor(UpdateAuthorRequest) returns (UpdateAuthorResponse);
  // Deletes a author by its ID.
  rpc DeleteAuthor(DeleteAuthorRequest) returns (google.protobuf.Empty);
}

// =============================================================================
// Service request and response messages
// =============================================================================

// Request message for CreateAuthor method.
message CreateAuthorRequest {
  // UUID used to prevent duplicate requests.
  optional string request_id = 1;
  // The author to be created.
  Author author = 2;
}

// Response message for CreateAuthor method.
message CreateAuthorResponse {
  // The created author.
  Author author = 1;
}

// Request message for GetAuthor method.
message GetAuthorRequest {
  // ID of the author to retrieve.
  string author_id = 1;
}

// Response message for GetAuthor method.
message GetAuthorResponse {
  // The retrieved author.
  Author author = 1;
}

// Request message for ListAuthors method.
message ListAuthorsRequest {
  // The maximum number of authors to return.
  optional int32 page_size = 1;
  // The token to retrieve the next page of results.
  optional string page_token = 2;
}

// Response message for ListAuthors method.
message ListAuthorsResponse {
  // The list of authors.
  repeated Author results = 1;
  // Token to retrieve the next page of results.
  string next_page_token = 2;
  // Estimated total number of results.
  int32 total_results_estimate = 3;
}

// Request message for UpdateAuthor method.
message UpdateAuthorRequest {
  // UUID used to prevent duplicate requests.
  optional string request_id = 1;
  // ID of the author to update.
  string author_id = 2;
  // The author to be updated.
  Author author = 3;
  // The field mask specifying the fields to update.
  google.protobuf.FieldMask update_mask = 4;
}

// Response message for UpdateAuthor method.
message UpdateAuthorResponse {
  // The updated author.
  Author author = 1;
}

// Request message for DeleteAuthor method.
message DeleteAuthorRequest {
  // ID of the author to delete.
  string id = 1;
}

// =============================================================================
// Resource messages
// =============================================================================

// The Author message represents a author in the library.
message Author {
  // Unique identifier for the author.
  string id = 1;
  // When the author was created.
  google.protobuf.Timestamp create_time = 2;
  // When the author was last updated.
  google.protobuf.Timestamp update_time = 3;
  // Additional fields for the author.
  // TODO: Add fields here.
}

TypeScript with ts-rest

Note

Learn more about ts-rest: https://ts-rest.com/

Create book contract with author parent resource:

apigen ts-rest -r book -p author -w

This will generate the following standard API definitions in contracts/bookContract.ts:

import { initContract } from '@ts-rest/core';
import { z } from 'zod';

// ====================================================================================================================
// Schemas
// ====================================================================================================================

// Timestamps using ISO 8601 format
const Timestamp = z.string().datetime().describe('ISO 8601 format timestamp');

// Book schema
export const Book = z.object({
  id: z.string().ulid().describe('Unique identifier for the book'),
  createTime: Timestamp.describe('When the book was created'),
  updateTime: Timestamp.describe('When the book was last updated'),
  authorId: z.string().ulid().describe('ID of the author of the book'),
});

// ====================================================================================================================
// Request and response schemas
// ====================================================================================================================

export const CreateBookRequest = z.object({
  requestId: z.string().uuid().optional().describe('UUID used to prevent duplicate requests'),
  book: Book.omit({ id: true, createTime: true, updateTime: true }),
});

export const CreateBookResponse = z.object({
  book: Book,
});

export const GetBookRequest = z.object({
  bookId: z.string().ulid().describe('The unique identifier of the book'),
});

export const GetBookResponse = z.object({
  book: Book,
});

export const ListBooksRequest = z.object({
  pageSize: z.number().int().min(1).max(1000).default(100).describe('The maximum number of books to return'),
  pageToken: z.string().optional().describe('The token to retrieve the next page of results'),
  authorId: z.string().ulid().optional().describe('The unique identifier of the parent author resource'),
});

export const ListBooksResponse = z.object({
  results: z.array(Book).describe('The list of books'),
  nextPageToken: z.string().ulid().nullish().describe('The token to retrieve the next page of items'),
  totalResultsEstimate: z.number().int().min(0).describe('The estimated total number of results'),
});

export const UpdateBookRequest = z.object({
  requestId: z.string().uuid().optional().describe('UUID used to prevent duplicate requests'),
  bookId: z.string().ulid().describe('The unique identifier of the book to update'),
  book: Book.omit({ id: true, createTime: true, updateTime: true }).partial(),
});

export const UpdateBookResponse = z.object({
  book: Book,
});

export const DeleteBookRequest = z.object({
  bookId: z.string().ulid().describe('The unique identifier of the book to delete'),
});

export const DeleteBookResponse = z.null();

// ====================================================================================================================
// Contracts
// ====================================================================================================================

const c = initContract();

export const BookServiceContract = c.router({
  createBook: {
    method: 'POST',
    path: '/books',
    body: CreateBookRequest,
    responses: {
      201: CreateBookResponse,
    },
    summary: 'Create a book',
  },
  getBook: {
    method: 'GET',
    path: '/books/:bookId',
    pathParams: GetBookRequest,
    responses: {
      200: GetBookResponse,
    },
    summary: 'Get a book by ID',
  },
  listBooks: {
    method: 'GET',
    path: '/books',
    query: ListBooksRequest,
    responses: {
      200: ListBooksResponse,
    },
    summary: 'List all books',
  },
  updateBook: {
    method: 'PATCH',
    path: '/books/:bookId',
    pathParams: UpdateBookRequest.pick({ bookId: true }),
    body: UpdateBookRequest.omit({ bookId: true }),
    responses: {
      200: UpdateBookResponse,
    },
    summary: 'Update a book',
  },
  deleteBook: {
    method: 'DELETE',
    path: '/books/:bookId',
    pathParams: DeleteBookRequest,
    body: z.null(), // No body, but we need to specify it as null
    responses: {
      204: DeleteBookResponse,
    },
    summary: 'Delete a book',
  },
});

Create author contract withouth a parent resource and using uuid as id:

apigen ts-rest -r author --id uuid -w

This will generate the following standard API definitions in contracts/authorContract.ts:

import { initContract } from '@ts-rest/core';
import { z } from 'zod';

// ====================================================================================================================
// Schemas
// ====================================================================================================================

// Timestamps using ISO 8601 format
const Timestamp = z.string().datetime().describe('ISO 8601 format timestamp');

// Author schema
export const Author = z.object({
  id: z.string().uuid().describe('Unique identifier for the author'),
  createTime: Timestamp.describe('When the author was created'),
  updateTime: Timestamp.describe('When the author was last updated'),
});

// ====================================================================================================================
// Request and response schemas
// ====================================================================================================================

export const CreateAuthorRequest = z.object({
  requestId: z.string().uuid().optional().describe('UUID used to prevent duplicate requests'),
  author: Author.omit({ id: true, createTime: true, updateTime: true }),
});

export const CreateAuthorResponse = z.object({
  author: Author,
});

export const GetAuthorRequest = z.object({
  authorId: z.string().uuid().describe('The unique identifier of the author'),
});

export const GetAuthorResponse = z.object({
  author: Author,
});

export const ListAuthorsRequest = z.object({
  pageSize: z.number().int().min(1).max(1000).default(100).describe('The maximum number of authors to return'),
  pageToken: z.string().optional().describe('The token to retrieve the next page of results'),
});

export const ListAuthorsResponse = z.object({
  results: z.array(Author).describe('The list of authors'),
  nextPageToken: z.string().uuid().nullish().describe('The token to retrieve the next page of items'),
  totalResultsEstimate: z.number().int().min(0).describe('The estimated total number of results'),
});

export const UpdateAuthorRequest = z.object({
  requestId: z.string().uuid().optional().describe('UUID used to prevent duplicate requests'),
  authorId: z.string().uuid().describe('The unique identifier of the author to update'),
  author: Author.omit({ id: true, createTime: true, updateTime: true }).partial(),
});

export const UpdateAuthorResponse = z.object({
  author: Author,
});

export const DeleteAuthorRequest = z.object({
  authorId: z.string().uuid().describe('The unique identifier of the author to delete'),
});

export const DeleteAuthorResponse = z.null();

// ====================================================================================================================
// Contracts
// ====================================================================================================================

const c = initContract();

export const AuthorServiceContract = c.router({
  createAuthor: {
    method: 'POST',
    path: '/authors',
    body: CreateAuthorRequest,
    responses: {
      201: CreateAuthorResponse,
    },
    summary: 'Create a author',
  },
  getAuthor: {
    method: 'GET',
    path: '/authors/:authorId',
    pathParams: GetAuthorRequest,
    responses: {
      200: GetAuthorResponse,
    },
    summary: 'Get a author by ID',
  },
  listAuthors: {
    method: 'GET',
    path: '/authors',
    query: ListAuthorsRequest,
    responses: {
      200: ListAuthorsResponse,
    },
    summary: 'List all authors',
  },
  updateAuthor: {
    method: 'PATCH',
    path: '/authors/:authorId',
    pathParams: UpdateAuthorRequest.pick({ authorId: true }),
    body: UpdateAuthorRequest.omit({ authorId: true }),
    responses: {
      200: UpdateAuthorResponse,
    },
    summary: 'Update a author',
  },
  deleteAuthor: {
    method: 'DELETE',
    path: '/authors/:authorId',
    pathParams: DeleteAuthorRequest,
    body: z.null(), // No body, but we need to specify it as null
    responses: {
      204: DeleteAuthorResponse,
    },
    summary: 'Delete a author',
  },
});