Fastify Prisma REST backend
Szablon TypeScript dla REST API Fastify z autoryzacją JWT i Prisma.

Daniel Gustaw
• 7 min read

Szybki backend REST z Prisma
Tworzę wiele projektów z użyciem Fastify i w tym artykule zgromadziłem najczęściej występujące elementy tych projektów.
Ustawienie projektu:
pnpm init
pnpm --package=typescript dlx tsc --init
pnpm add -D typescript @types/node
opcjonalnie dodaj
{
"pnpm": {
"neverBuiltDependencies": []
}
}
do package.json
, aby uniknąć pytania pnpm
o zatwierdzenie przy każdym budowaniu.
Walidacja zmiennych środowiskowych
Nie chcemy zezwalać na serwowanie projektu, jeśli brakuje zmiennych środowiskowych. Dzięki zod
możemy zweryfikować, czy .env zawiera wszystkie wymagane wartości.
mkdir -p src && touch src/config.ts
pnpm add zod
dodaj plik 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);
Połączenie Prisma
Większość projektów potrzebuje bazy danych.
pnpm add prisma @prisma/client
npx prisma init
następnie zgódź się na wersje.
pnpm approve-builds
Synchronizuj schemat prisma, jeśli istnieje
pnpm dlx prisma db pull
touch src/db.ts
Dodaj Fastify
Odkomentuj linię "resolveJsonModule": true,
w tsconfig.json
.
Zainstaluj pakiety
pnpm add fastify @fastify/cors @fastify/sensible jsonwebtoken
pnpm add -D @types/jsonwebtoken tsx
Dodaj plik 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;
}
To jest minimalistyczna konfiguracja z walidacją użytkowników. Możemy zaimportować ją w 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}`);
},
);
dodaj skrypt dev
do package.json
{
"scripts": {
"dev": "infisical run --env=dev --path=/apps/fastify -- tsx watch ./src/index.ts",
}
}
Test przez
pnpm dev
i
http localhost:4747
Budowanie
pnpm add -D tsup
dodaj skrypty
{
"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"
}
}
Test E2E
pnpm add -D vitest
dodaj plik 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',
});
})
i skrypt
{
"scripts": {
"test": "infisical run --env=dev --path=/apps/fastify -- vitest run",
}
}
zadzwoń
pnpm test
Testuj chronioną trasę
utwórz trasę
mkdir -p src/controller/auth
touch src/controller/auth/me.ts
dodaj plik src/controller/auth/me.ts
import {FastifyRequest} from "fastify";
import {UserProjection} from "../../types/auth";
export function me(req: FastifyRequest): UserProjection | null {
return req.user;
}
gdzie src/types/auth.ts
zawiera typy wykluczone z fastify.ts
export interface TokenPayload {
id: number,
iat: number,
exp: number
}
export interface UserProjection {
id: number
}
Możemy również wyłączyć router.ts
z fastify.ts
do osobnego pliku.
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();
}
i przenieś version
do 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,
});
}
Teraz możemy to przetestować w pliku 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,
});
})
Wdrożenie
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
Plik 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 }}'
Przejdź do kontroli dostępu Infisical, wybierz token usługi i utwórz nowy z uprawnieniami read
, wybierając zakres /**
oraz czas wygaśnięcia nigdy
.
Na koniec dodaj polecenie deploy do Makefile
deploy:
ansible-playbook -i "hosts.${ENV}" deploy.yml
Aby powiązać wdrożenie z domeną na serwerze, dodaj do /etc/caddy/Caddyfile
linie
api.domain.com {
reverse_proxy localhost:4747
}
i ponownie załaduj przez
systemctl reload caddy
Other articles
You can find interesting also.

Jak wojna o kompatybilność ukształtowała frontend?
Opisujemy jak porzucanie i dbanie o kompatybilność wsteczną wpływało na kierunek rozwoju technologii webowych.

Daniel Gustaw
• 5 min read

Aplikacja z FOSUserBundle i API Google Maps
Prosta apka integrująca fos user bundle z google maps. Serwis pozwala na logowanie, rejestrację oraz zapisywanie swojej listy lokalizacji walidowanych przez api od google.

Daniel Gustaw
• 45 min read

Logowanie danych w MySql, Ajax i Behat
Napiszemy prostą aplikację webową - kalkulator. Na jego przykładzie pokażemy jak skonfigurować selenium z behatem i wykonać na nim testy automatyczne.

Daniel Gustaw
• 15 min read