Backend REST de Fastify Prisma
Plantilla de TypeScript para API REST de Fastify con autenticación JWT y Prisma.

Daniel Gustaw
• 7 min read

Fastify Prisma REST backend
Estoy creando muchos proyectos de Fastify y en este artículo he reunido las partes más comunes de ellos.
Configuración del proyecto:
pnpm init
pnpm --package=typescript dlx tsc --init
pnpm add -D typescript @types/node
opcionalmente agregar
{
"pnpm": {
"neverBuiltDependencies": []
}
}
a package.json
para evitar que pnpm
pida aprobación para cada compilación.
Validación de entorno
No queremos permitir la ejecución del proyecto si faltan variables de entorno. Gracias a zod
podemos validar si .env contiene todos los valores requeridos.
mkdir -p src && touch src/config.ts
pnpm add zod
agregar archivo src/config.ts
import { z } from 'zod';
export const serverVariables = z.object({
DATABASE_URL: z.string(),
JWT_SECRET: z.string(),
NODE_ENV: z
.enum(['development', 'production', 'test'])
.default('development'),
PORT: z.coerce.number().int().default(4747),
});
export const config = serverVariables.parse(process.env);
Conexión de Prisma
La mayoría de los proyectos necesita una base de datos.
pnpm add prisma @prisma/client
npx prisma init
entonces acuerde para las compilaciones.
pnpm approve-builds
Sincroniza el esquema de prisma si existe
pnpm dlx prisma db pull
touch src/db.ts
Agregar Fastify
Descomenta la línea "resolveJsonModule": true,
en tsconfig.json
.
Instala paquetes
pnpm add fastify @fastify/cors @fastify/sensible jsonwebtoken
pnpm add -D @types/jsonwebtoken tsx
Añadir archivo src/fastify.ts
import fastify, {
FastifyInstance, FastifyPluginOptions,
FastifyReply,
FastifyRequest, RouteShorthandOptions,
} from 'fastify';
import {config} from "./config";
import cors from '@fastify/cors';
import fastifySensible from '@fastify/sensible';
import {FastifyRouteConfig} from "fastify/types/route";
import jwt from 'jsonwebtoken';
import pJson from '../package.json';
interface TokenPayload {
id: number,
iat: number,
exp: number
}
interface UserProjection {
id: number
}
declare module 'fastify' {
interface FastifyRequest {
user: UserProjection | null;
}
}
export function verifyToken(token: string): UserProjection {
const payload = jwt.verify(token, config.JWT_SECRET) as TokenPayload;
return {
id: payload.id,
}
}
function isProtected(config: FastifyRouteConfig): boolean {
return (
Boolean('isProtected' in config && config.isProtected)
);
}
function getErrorMessage(error: unknown): string {
if(error instanceof Error) {
return error.message;
}
return 'Unknown error';
}
function version(_req: FastifyRequest, res: FastifyReply): void {
res.code(200).send({
name: pJson.name,
version: pJson.version,
});
};
const PROTECTED: RouteShorthandOptions = { config: { isProtected: true } };
const PUBLIC: RouteShorthandOptions = { config: { isProtected: false } };
function router(
server: FastifyInstance,
_options: FastifyPluginOptions,
next: () => void,
) {
server.get('/', PUBLIC, version);
next();
}
export function getFastifyInstance(): FastifyInstance {
const app = fastify({
logger: config.NODE_ENV === 'development',
bodyLimit: 100 * 1048576,
});
app.register(cors, {});
app.register(fastifySensible);
app.addHook(
'onRequest',
async (
request: FastifyRequest<{ Headers: { authorization?: string } }>,
reply: FastifyReply,
) => {
// If the route is not private we ignore this hook
if (isProtected(request.routeOptions.config)) {
const authHeader = request.headers.authorization;
if (typeof authHeader !== 'string') {
reply.unauthorized('No Authorization header');
return;
}
const token: string = String(authHeader)
.replace(/^Bearer\s+/, '')
.trim();
if (!token) {
reply.unauthorized('Token is empty');
return;
}
try {
request.user = verifyToken(token);
} catch (error) {
return reply.unauthorized(getErrorMessage(error));
}
}
},
);
app.register(router);
return app;
}
Esta es una configuración minimalista con validación de usuarios. Podemos importarlo en src/index.ts
import {getFastifyInstance} from './fastify';
import { config } from './config';
const app = getFastifyInstance();
app.listen(
{ port: config.PORT, host: '0.0.0.0' },
(err: Error | null, host: string) => {
if (err) {
throw err;
}
console.info(`server listening on ${host}`);
},
);
agregar script dev
a package.json
{
"scripts": {
"dev": "infisical run --env=dev --path=/apps/fastify -- tsx watch ./src/index.ts",
}
}
Prueba por
pnpm dev
y
http localhost:4747
Construcción
pnpm add -D tsup
agregar scripts
{
"scripts": {
"build": "tsup src/index.ts",
"serve:prod": "infisical run --domain https://infisical.preciselab.io --projectId acb5ccfb-c211-4461-a06d-8caa248beea1 --env=prod --path=/apps/fastify -- node dist/index.js"
}
}
Prueba E2E
pnpm add -D vitest
agregar archivo test/version.e2e.spec.ts
import {it, expect} from "vitest";
import {getFastifyInstance} from "../src/fastify";
it('should return version', async () => {
const app = getFastifyInstance();
const response = await app.inject({
method: 'GET',
url: '/',
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
name: 'fastify',
version: '1.0.0',
});
})
y guion
{
"scripts": {
"test": "infisical run --env=dev --path=/apps/fastify -- vitest run",
}
}
llamar
pnpm test
Probar ruta protegida
crear ruta
mkdir -p src/controller/auth
touch src/controller/auth/me.ts
agregar archivo src/controller/auth/me.ts
import {FastifyRequest} from "fastify";
import {UserProjection} from "../../types/auth";
export function me(req: FastifyRequest): UserProjection | null {
return req.user;
}
donde src/types/auth.ts
contiene tipos excluidos de fastify.ts
export interface TokenPayload {
id: number,
iat: number,
exp: number
}
export interface UserProjection {
id: number
}
También podemos excluir router.ts
de fastify.ts
a un archivo separado.
import {FastifyInstance, FastifyPluginOptions, RouteShorthandOptions} from "fastify";
import {version} from "./controllers/app/version";
import {me} from "./controllers/auth/me";
const PROTECTED: RouteShorthandOptions = { config: { isProtected: true } };
const PUBLIC: RouteShorthandOptions = { config: { isProtected: false } };
export function router(
server: FastifyInstance,
_options: FastifyPluginOptions,
next: () => void,
) {
server.get('/', PUBLIC, version);
server.get('/me', PROTECTED, me);
next();
}
y mueve version
a controllers/app/version.ts
import {FastifyReply, FastifyRequest} from "fastify";
import pJson from "../../../package.json";
export function version(_req: FastifyRequest, res: FastifyReply): void {
res.code(200).send({
name: pJson.name,
version: pJson.version,
});
}
Ahora podemos probarlo en el archivo test/auth.e2e.spec.ts
import {it, expect} from "vitest";
import {getFastifyInstance} from "../src/fastify";
import jwt from "jsonwebtoken";
import {config} from "../src/config";
it('should return me', async () => {
const app = getFastifyInstance();
const token = jwt.sign({
"id": 6,
"iat": 1739986214,
"exp": (Date.now() / 1000) + 3600 // 1 hour from now
}, config.JWT_SECRET);
const response = await app.inject({
method: 'GET',
url: '/me',
headers: {
authorization: `Bearer ${token}`
}
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
id: 6,
});
})
Implementación
Dockerfile
FROM node:22-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare [email protected] --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
FROM base AS build
COPY . .
RUN pnpm dlx prisma generate
RUN pnpm build
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical
CMD pnpm serve:prod
Docker compose: docker-compose.yml
services:
app:
image: registry.digitalocean.com/main/up-fastify
ports:
- '4747:4747'
environment:
- INFISICAL_TOKEN
Archivo de hosts
[local]
127.0.0.1 env=prod ansible_python_interpreter=/usr/bin/python3
[api]
142.132.182.19 ansible_user=root env=prod ansible_python_interpreter=/usr/bin/python3
Ansible playbook deploy.yml
---
- name: Build Backend
hosts: local
connection: local
vars:
image_url: registry.digitalocean.com/main/api-domain-com
tasks:
- name: Build Image
ansible.builtin.shell: >
DOCKER_BUILDKIT=1 docker build -t {{image_url}} .
- name: Push Image
ansible.builtin.shell: >
docker push {{image_url}}
- name: Deploy Backend
hosts: api
vars:
path: /root/api.domain.com
tasks:
- name: Creates Api directory
file:
path: '{{ path }}'
state: directory
- name: Copy Docker Compose
copy:
src: ./docker-compose.yml
dest: '{{ path }}/docker-compose.yml'
- name: Pull Image
shell:
cmd: docker compose pull
chdir: '{{ path }}'
- name: Restart Image
shell:
cmd: docker compose up -d --remove-orphans
chdir: '{{ path }}'
Ve a control de acceso de Infisical, selecciona el token de servicio y crea uno nuevo con permisos de lectura
seleccionando el alcance /**
y el tiempo de expiración nunca
.
Finalmente, agrega el comando de despliegue al Makefile
.
deploy:
ansible-playbook -i "hosts.${ENV}" deploy.yml
Luego, teniendo hosts.prod
y hosts.stag
, puedes desplegar ejecutando ENV=prod make deploy
o ENV=stag make deploy
.
Para vincular el despliegue con el dominio en el servidor, añade al archivo /etc/caddy/Caddyfile
las líneas
api.domain.com {
reverse_proxy localhost:4747
}
y recargar por
systemctl reload caddy
Other articles
You can find interesting also.

Patrón pull-push de ZeroMQ para Node JS
El artículo enfatiza la flexibilidad de ZeroMQ para la mensajería en Node.js, destacando el patrón pull-push ideal para sistemas distribuidos de alto volumen.

Daniel Gustaw
• 4 min read

Comunicación entre componentes de Vue en Meteor
Hay pocos métodos para enviar datos entre componentes de Vue no relacionados. Algunos de estos son universales, otros típicos de Vue y otros para Meteor. Compararemos todos ellos.

Daniel Gustaw
• 11 min read

Análisis de registros de Apache con GoAccess
En esta publicación, muestro una herramienta que permite extraer información interesante de archivos generados automáticamente durante el funcionamiento del servidor.

Daniel Gustaw
• 21 min read