@natiwo/api
v0.1.0
Published
NATIWO API - tRPC e NestJS helpers
Readme
@natiwo/api
tRPC setup and utilities for type-safe APIs
Installation
pnpm add @natiwo/api @trpc/server @trpc/clientFeatures
- 🚀 Pre-configured tRPC - Ready-to-use tRPC setup
- 🔐 Authentication Middleware - Built-in auth procedures
- 🛡️ Role-based Access - Admin and user procedures
- ✅ Zod Integration - Input validation out of the box
- 📊 Error Formatting - Enhanced error responses
Quick Start
Create Your First Router
import { createRouter, publicProcedure, protectedProcedure } from '@natiwo/api';
import { z } from 'zod';
export const userRouter = createRouter({
// Public endpoint - no auth required
getAll: publicProcedure
.query(async () => {
return await db.user.findMany();
}),
// Protected endpoint - requires authentication
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
// ctx.user is available and typed!
return await db.user.findUnique({
where: { id: input.id }
});
}),
// Create user
create: protectedProcedure
.input(z.object({
email: z.string().email(),
name: z.string(),
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
}),
});Merge Multiple Routers
import { mergeRouters } from '@natiwo/api';
import { userRouter } from './routers/user';
import { postRouter } from './routers/post';
export const appRouter = mergeRouters({
users: userRouter,
posts: postRouter,
});
export type AppRouter = typeof appRouter;Admin-Only Endpoints
import { adminProcedure } from '@natiwo/api';
export const adminRouter = createRouter({
deleteUser: adminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
// Only admins can call this
return await db.user.delete({ where: { id: input.id } });
}),
});Context Setup
The context is automatically typed and includes:
interface Context {
user?: UserSession; // Available in protectedProcedure
req?: unknown; // HTTP request
res?: unknown; // HTTP response
}Custom Context
import { initTRPC } from '@natiwo/api';
interface MyContext extends Context {
db: PrismaClient;
redis: Redis;
}
const t = initTRPC.context<MyContext>().create();
export const myRouter = t.router;
export const myProcedure = t.procedure;Client Usage
Next.js App Router
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();// app/page.tsx
'use client';
import { trpc } from '@/lib/trpc';
export default function Home() {
const { data: users } = trpc.users.getAll.useQuery();
return <div>{users?.map(u => <div key={u.id}>{u.name}</div>)}</div>;
}React (Vite)
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server/routers/_app';
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
],
});
// Usage
const users = await trpc.users.getAll.query();
const user = await trpc.users.create.mutate({
email: '[email protected]',
name: 'John Doe',
});Error Handling
import { TRPCError } from '@natiwo/api';
export const userRouter = createRouter({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.id }
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
});
}
return user;
}),
});Error Codes
BAD_REQUEST- Invalid input (400)UNAUTHORIZED- Not authenticated (401)FORBIDDEN- Not authorized (403)NOT_FOUND- Resource not found (404)TIMEOUT- Request timeout (408)CONFLICT- Resource conflict (409)PRECONDITION_FAILED- Precondition failed (412)PAYLOAD_TOO_LARGE- Payload too large (413)TOO_MANY_REQUESTS- Rate limited (429)CLIENT_CLOSED_REQUEST- Client closed (499)INTERNAL_SERVER_ERROR- Server error (500)
Middleware
import { createRouter } from '@natiwo/api';
const loggerMiddleware = t.middleware(async ({ path, next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(`[${path}] ${duration}ms`);
return result;
});
export const loggedProcedure = t.procedure.use(loggerMiddleware);Integration with @natiwo/auth
import { JWTManager } from '@natiwo/auth';
import { TRPCError } from '@natiwo/api';
const jwt = new JWTManager(process.env.JWT_SECRET);
export async function createContext({ req }: { req: Request }) {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return { user: undefined };
}
try {
const user = jwt.verify(token);
return { user };
} catch {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid token',
});
}
}Best Practices
Use input validation always
// ✅ Good publicProcedure .input(z.object({ email: z.string().email() })) .mutation(async ({ input }) => { ... });Separate concerns with routers
const appRouter = mergeRouters({ users: userRouter, posts: postRouter, comments: commentRouter, });Use proper error codes
if (!found) { throw new TRPCError({ code: 'NOT_FOUND' }); }Leverage TypeScript inference
// Type is inferred automatically! const user = await trpc.users.getById.query({ id: '123' });
License
MIT © NATIWO Sistemas
