Skip to content

Commit

Permalink
Fix misc typings, RPC logic, OpenAPI generation etc. (#108)
Browse files Browse the repository at this point in the history
This bumps the Next.js version to 14 and
adds various typing improvements, OpenAPI
generation, RPC and docs improvements for
client implementation.
  • Loading branch information
blomqma authored Dec 13, 2023
1 parent c140597 commit 6f16b75
Show file tree
Hide file tree
Showing 54 changed files with 2,166 additions and 1,738 deletions.
268 changes: 167 additions & 101 deletions apps/example/public/openapi.json

Large diffs are not rendered by default.

83 changes: 83 additions & 0 deletions apps/example/src/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use server';

import { rpcOperation } from 'next-rest-framework';
import { MOCK_TODOS, todoSchema } from 'utils';
import { z } from 'zod';

// The RPC operations can be used as server-actions and imported in the RPC route handlers.

export const getTodos = rpcOperation({
tags: ['RPC']
})
.outputs([
{
schema: z.array(todoSchema)
}
])
.handler(() => {
return MOCK_TODOS; // Type-checked output.
});

export const getTodoById = rpcOperation({
tags: ['RPC']
})
.input(z.string())
.outputs([
{
schema: z.object({
error: z.string()
})
},
{
schema: todoSchema
}
])
.handler((id) => {
const todo = MOCK_TODOS.find((t) => t.id === Number(id));

if (!todo) {
return { error: 'TODO not found.' }; // Type-checked output.
}

return todo; // Type-checked output.
});

export const createTodo = rpcOperation({
tags: ['RPC']
})
.input(
z.object({
name: z.string()
})
)
.outputs([{ schema: todoSchema }])
.handler(
async ({
name // Strictly-typed input.
}) => {
// Create todo.
const todo = { id: 2, name, completed: false };
return todo; // Type-checked output.
}
);

export const deleteTodo = rpcOperation({
tags: ['RPC']
})
.input(z.string())
.outputs([
{ schema: z.object({ error: z.string() }) },
{ schema: z.object({ message: z.string() }) }
])
.handler((id) => {
// Delete todo.
const todo = MOCK_TODOS.find((t) => t.id === Number(id));

if (!todo) {
return {
error: 'TODO not found.' // Type-checked output.
};
}

return { message: 'TODO deleted.' }; // Type-checked output.
});
6 changes: 0 additions & 6 deletions apps/example/src/app/api/route.ts

This file was deleted.

96 changes: 0 additions & 96 deletions apps/example/src/app/api/routes/rpc/route.ts

This file was deleted.

6 changes: 6 additions & 0 deletions apps/example/src/app/api/v2/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { docsRoute } from 'next-rest-framework';

export const { GET } = docsRoute({
deniedPaths: ['/api/routes/third-party-endpoint'],
openApiJsonPath: '/openapi.json'
});
13 changes: 13 additions & 0 deletions apps/example/src/app/api/v2/rpc/[operationId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createTodo, deleteTodo, getTodoById, getTodos } from 'actions';
import { rpcRoute } from 'next-rest-framework';

const { POST, client } = rpcRoute({
getTodos,
getTodoById,
createTodo,
deleteTodo
});

export type RpcClient = typeof client;

export { POST };
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
TypedNextResponse,
routeHandler,
routeOperation
} from 'next-rest-framework';
import { TypedNextResponse, route, routeOperation } from 'next-rest-framework';
import { z } from 'zod';

const TODOS = [
Expand All @@ -14,10 +10,13 @@ const TODOS = [
];

// Example dynamic App Router route handler with GET/DELETE handlers.
export const GET = routeHandler({
GET: routeOperation({
operationId: 'getTodoById',
tags: ['example-api', 'todos', 'app-router']
export const { GET, DELETE } = route({
getTodoById: routeOperation({
method: 'GET',
// Optional OpenAPI operation documentation.
openApiOperation: {
tags: ['example-api', 'todos', 'app-router']
}
})
.outputs([
{
Expand Down Expand Up @@ -49,9 +48,12 @@ export const GET = routeHandler({
});
}),

DELETE: routeOperation({
operationId: 'deleteTodo',
tags: ['example-api', 'todos', 'app-router']
deleteTodo: routeOperation({
method: 'DELETE',
// Optional OpenAPI operation documentation.
openApiOperation: {
tags: ['example-api', 'todos', 'app-router']
}
})
.outputs([
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,37 @@
import {
TypedNextResponse,
routeHandler,
routeOperation
} from 'next-rest-framework';
import { TypedNextResponse, route, routeOperation } from 'next-rest-framework';
import { MOCK_TODOS, todoSchema } from 'utils';
import { z } from 'zod';

const TODOS = [
{
id: 1,
name: 'TODO 1',
completed: false
}
];

// Example App Router route handler with GET/POST handlers.
const handler = routeHandler({
GET: routeOperation({
const { GET, POST } = route({
getTodos: routeOperation({
method: 'GET',
// Optional OpenAPI operation documentation.
operationId: 'getTodos',
tags: ['example-api', 'todos', 'app-router']
openApiOperation: {
tags: ['example-api', 'todos', 'app-router']
}
})
// Output schema for strictly-typed responses and OpenAPI documentation.
.outputs([
{
status: 200,
contentType: 'application/json',
schema: z.array(
z.object({
id: z.number(),
name: z.string(),
completed: z.boolean()
})
)
schema: z.array(todoSchema)
}
])
.handler(() => {
// Type-checked response.
return TypedNextResponse.json(TODOS, {
return TypedNextResponse.json(MOCK_TODOS, {
status: 200
});
}),

POST: routeOperation({
createTodo: routeOperation({
method: 'POST',
// Optional OpenAPI operation documentation.
operationId: 'createTodo',
tags: ['example-api', 'todos', 'app-router']
openApiOperation: {
tags: ['example-api', 'todos', 'app-router']
}
})
// Input schema for strictly-typed request, request validation and OpenAPI documentation.
.input({
Expand All @@ -66,15 +53,17 @@ const handler = routeHandler({
schema: z.string()
}
])
// Optional middleware logic executed before request validation.
.middleware((req) => {
if (!req.headers.get('authorization')) {
// Type-checked response.
return TypedNextResponse.json('Unauthorized', {
status: 401
});
.middleware(
// Optional middleware logic executed before request validation.
(req) => {
if (!req.headers.get('authorization')) {
// Type-checked response.
return TypedNextResponse.json('Unauthorized', {
status: 401
});
}
}
})
)
.handler(async (req) => {
const { name } = await req.json(); // Strictly-typed request.

Expand All @@ -85,4 +74,4 @@ const handler = routeHandler({
})
});

export { handler as GET, handler as POST };
export { GET, POST };
38 changes: 38 additions & 0 deletions apps/example/src/app/client/ClientExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { type RpcClient } from 'app/api/v2/rpc/[operationId]/route';
import { rpcClient } from 'next-rest-framework/dist/client/rpc-client';
import { useEffect, useState } from 'react';
import { type Todo } from 'utils';

const client = rpcClient<RpcClient>({
url: 'http://localhost:3000/api/v2/rpc'
});

export const ClientExample: React.FC = () => {
const [loading, setLoading] = useState(true);
const [todos, setTodos] = useState<Todo[]>([]);

useEffect(() => {
client
.getTodos()
.then(setTodos)
.catch(console.error)
.finally(() => {
setLoading(false);
});
}, []);

return (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-bold">RPC client example</h1>
{loading ? (
<p>Loading...</p>
) : (
<>
<p>Data:</p> <p>{JSON.stringify(todos)}</p>
</>
)}
</div>
);
};
Loading

2 comments on commit 6f16b75

@vercel
Copy link

@vercel vercel bot commented on 6f16b75 Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-rest-framework – ./docs

next-rest-framework-git-main-blomqma.vercel.app
next-rest-framework.vercel.app
next-rest-framework-blomqma.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 6f16b75 Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-rest-framework-demo – ./apps/example

next-rest-framework-demo.vercel.app
next-rest-framework-demo-blomqma.vercel.app
next-rest-framework-demo-git-main-blomqma.vercel.app

Please sign in to comment.