Skip to content

Instantly share code, notes, and snippets.

@ivawzh
Created March 17, 2024 04:13
Show Gist options
  • Save ivawzh/daa0c65ac09cdb116ec3c95b653cc6f5 to your computer and use it in GitHub Desktop.
Save ivawzh/daa0c65ac09cdb116ec3c95b653cc6f5 to your computer and use it in GitHub Desktop.
TS-Rest + NextJS
// pages/api/[...ts-rest].tsx
import { createNextRoute as fulfilContract, createNextRouter } from '@ts-rest/next';
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
export interface Post {
id: string;
title: string;
description: string | null;
content: string | null;
published: boolean;
tags: string[];
}
const PostSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().nullable(),
content: z.string().nullable(),
published: z.boolean(),
tags: z.array(z.string()),
});
const tsRest = initContract();
const createContract = tsRest.router;
export const postApiContract = createContract(
{
createPost: {
method: 'POST',
path: '/posts',
responses: {
201: PostSchema,
400: z.object({ message: z.string() }),
},
body: z.object({
title: z.string().transform((v) => v.trim()),
content: z.string(),
published: z.boolean().optional(),
description: z.string().optional(),
}),
summary: 'Create a post',
metadata: { roles: ['user'] } as const,
},
updatePost: {
method: 'PATCH',
path: '/posts/:id',
responses: { 200: PostSchema },
body: z.object({
title: z.string().optional(),
content: z.string().optional(),
published: z.boolean().optional(),
description: z.string().optional(),
}),
summary: 'Update a post',
metadata: {
roles: ['user'],
resource: 'post',
identifierPath: 'params.id',
} as const,
},
deletePost: {
method: 'DELETE',
path: '/posts/:id',
responses: {
200: z.object({ message: z.string() }),
404: z.object({ message: z.string() }),
},
body: null,
summary: 'Delete a post',
metadata: {
roles: ['user'],
resource: 'post',
identifierPath: 'params.id',
} as const,
},
getPost: {
method: 'GET',
path: '/posts/:id',
responses: {
200: PostSchema,
404: z.null(),
},
query: null,
summary: 'Get a post by id',
metadata: { roles: ['guest', 'user'] } as const,
},
getPosts: {
method: 'GET',
path: '/posts',
responses: {
200: z.object({
posts: PostSchema.array(),
count: z.number(),
skip: z.number(),
take: z.number(),
}),
},
query: z.object({
take: z.string().transform(Number),
skip: z.string().transform(Number),
search: z.string().optional(),
}),
summary: 'Get all posts',
headers: z.object({
'x-pagination': z.coerce.number().optional(),
}),
metadata: { roles: ['guest', 'user'] } as const,
},
testPathParams: {
method: 'GET',
path: '/test/:id/:name',
pathParams: z.object({
id: z
.string()
.transform(Number)
.refine((v) => Number.isInteger(v), {
message: 'Must be an integer',
}),
}),
query: z.object({
field: z.string().optional(),
}),
responses: {
200: z.object({
id: z.number().lt(1000),
name: z.string(),
defaultValue: z.string().default('hello world'),
}),
},
metadata: { roles: ['guest', 'user'] } as const,
},
},
{
// baseHeaders: z.object({
// 'x-api-key': z.string(),
// }),
}
);
const apiHealthContract = createContract({
check: {
method: 'GET',
path: '/health',
responses: {
200: tsRest.response<{ message: string }>(),
},
query: null,
summary: 'Check health',
},
});
const multiServiceContract = createContract({
/**
* Posts API
*/
posts: postApiContract,
/**
* Health API
*/
health: apiHealthContract,
});
const postsRouter = fulfilContract(multiServiceContract.posts, {
createPost: async (args) => {
const newPost = {
id: 'mock_id',
title: 'mock_title',
description: 'mock_description',
content: 'mock_content',
published: true,
tags: ['mock_tags'],
}
return {
status: 201,
body: newPost,
};
},
updatePost: async () => {
return {
status: 200,
body: {
id: '1',
title: 'title',
tags: [],
description: '',
content: '',
published: false,
},
};
},
deletePost: async (args) => {
return {
status: 200,
body: { message: 'Post deleted' },
};
},
getPost: async ({ params }) => {
const post = {
id: 'mock_id',
title: 'mock_title',
description: 'mock_description',
content: 'mock_content',
published: true,
tags: ['mock_tags'],
};
if (!post) {
return {
status: 404,
body: null,
};
}
return {
status: 200,
body: post,
};
},
getPosts: async (args) => {
return {
status: 200,
body: {
posts: [{
id: 'mock_id',
title: 'mock_title',
description: 'mock_description',
content: 'mock_content',
published: true,
tags: ['mock_tags'],
}],
count: 10,
skip: args.query.skip,
take: args.query.take,
},
};
},
testPathParams: async (args) => {
return {
status: 200,
body: args.params,
};
},
});
const healthRouter = fulfilContract(multiServiceContract.health, {
check: async (args) => {
return {
status: 200,
body: { message: 'OK' },
};
},
});
const router = fulfilContract(multiServiceContract, {
posts: postsRouter,
health: healthRouter,
});
export default createNextRouter(multiServiceContract, router);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment