Bun REST API можно освоить на одном небольшом проекте, если сразу видеть вход, действие и результат. Встроенный Bun.serve принимает HTTP-запросы и возвращает стандартные Response, поэтому первый API помещается в один TypeScript-файл. Ниже вы соберёте работающий пример и проверите обычный и граничный сценарии.
Express часто добавляют по привычке, хотя для учебных GET и POST достаточно возможностей самого runtime. Поэтому отделим настройку, входные данные, основное действие и проверку. В этой статье вы соберёте API заметок с GET /notes, POST /notes, валидацией JSON и корректными статусами. Проверка: GET возвращает массив, валидный POST создаёт заметку со статусом 201, а пустой title получает 400.
установить Bun и создать server.ts. Не расширяйте проект до первого успешного запуска.
curl получает статус 200 и массив. Проверяйте значение, а не отсутствие ошибки.
После успеха добавьте поле done и PATCH-маршрут, который меняет только существующую заметку. Так копирование станет практикой.
Как устроен учебный проект Bun REST API
HTTP-сервер передаёт Request в функцию fetch, роутер выбирает действие по методу и URL, а каждая ветка возвращает Response. Держите ввод, логику и отображение раздельно: тогда каждый этап можно проверить отдельно.
| Этап | Действие | Проверяемый результат |
|---|---|---|
| Среда | установить Bun и создать server.ts | bun --version и bun run server.ts работают |
| Чтение | обработать GET /notes и вернуть JSON | curl получает статус 200 и массив |
| Создание | прочитать JSON в POST и добавить запись | ответ содержит новую заметку и статус 201 |
| Ошибка | отклонить пустой title и неизвестный путь | клиент получает 400 или 404 с понятным JSON |
Контрольные точки: среда, чтение, создание и ошибка. Следующий шаг начинается после совпадения с ожидаемым результатом.

От входных данных до видимого результата. Сначала проверяется вход, затем действие и только потом интерфейс.
http://localhost:3000/notes. До добавления POST должен вернуться пустой JSON-массив. Сохраните команду запуска и ожидаемый вывод.Собираем рабочий пример шаг за шагом
Пример сверялся с актуальной документацией официальной документации Bun.serve на 19 октября 2026 года. Установите Bun по официальной инструкции, затем создайте папку и выполните bun init -y. Проверьте текущую папку через pwd или Get-Location. Вставьте пример целиком, выполните один раз, затем разбирайте по частям.
type Note = { id: number; title: string }
const notes: Note[] = []
Bun.serve({
port: 3000,
async fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/notes" && request.method === "GET") {
return Response.json(notes)
}
if (url.pathname === "/notes" && request.method === "POST") {
const body = await request.json() as { title?: string }
const title = body.title?.trim()
if (!title) return Response.json({ error: "title required" }, { status: 400 })
const note = { id: notes.length + 1, title }
notes.push(note)
return Response.json(note, { status: 201 })
}
return Response.json({ error: "not found" }, { status: 404 })
},
})
console.log("http://localhost:3000")- GET
/notesотвечает[], а после создания содержит новую заметку. - POST с
{"title":"Изучить Bun"}возвращает 201 и объект с числовым id. - Повторный запуск даёт тот же предсказуемый результат.
- Пустой ввод получает понятное сообщение вместо падения.
Bun использует знакомые веб-объекты Request, Response и URL. Поэтому основная логика не зависит от Express API. Найдите в примере вход, проверку и место формирования результата.

Что отличает устойчивый проект от случайного запуска. Явные границы делают ошибку локальной и понятной.
Что происходит внутри и почему это работает
Обработчик fetch вызывается для каждого запроса. URL отделяет путь от полного адреса, метод различает чтение и создание, а Response задаёт тело, заголовки и статус. Логику держите отдельно от вывода: функция получает аргумент и возвращает значение, а интерфейс показывает его. Массив хранится только в памяти процесса. После перезапуска данные исчезнут, и это нормальное ограничение учебного примера, а не скрытая база.
| Часть | За что отвечает | Что проверить |
|---|---|---|
| Маршрутизация | сравнивает pathname и method | неизвестный путь получает ровно 404 |
| Валидация | читает JSON и проверяет непустой title | пустая строка и отсутствующее поле получают 400 |
| Ответ | формирует JSON и правильный HTTP-статус | клиент различает чтение, создание и ошибку |
Не ловите все ошибки одним пустым catch. Невалидный JSON отличается от пустого title и ошибки сервера. В следующей версии оберните чтение тела, верните 400 для синтаксически плохого JSON и оставьте 500 только для неожиданной ситуации. Не прячьте поток в одном обработчике: сохраните вход, проверьте его, вызовите операцию и только затем обновите интерфейс.
curl -i -X POST localhost:3000/notes -H "content-type: application/json" -d "{} и проверьте статус 400. Меняйте одно условие за прогон, чтобы видеть причину.Проверяем проект и добавляем свою функцию
Добавьте DELETE /notes/:id. Сначала разберите id из пути, затем проверьте существование заметки и только после этого удаляйте. До правки запишите ожидаемое поведение одним предложением. После граничного теста повторите обычный сценарий.
- Сохраните ожидаемый результат: GET возвращает массив, валидный POST создаёт заметку со статусом 201, а пустой title получает 400.
- Измените правило: добавьте поле done и PATCH-маршрут, который меняет только существующую заметку. Сначала предскажите итог.
- Передайте граничное значение и убедитесь, что приложение не падает.
- Верните корректные данные и объясните роли трёх основных частей.
- bun --version и bun run server.ts работают
- curl получает статус 200 и массив
- ответ содержит новую заметку и статус 201
- клиент получает 400 или 404 с понятным JSON
Фреймворк стоит добавлять, когда его маршрутизация, плагины или соглашения решают реальную проблему, а не до первого запроса. Полезная правка проходит от нового ввода до видимого результата.

Четыре проверки перед следующим шагом. Каждый следующий этап начинается после наблюдаемого результата.
Что запомнить и куда двигаться дальше
После учебного массива подключите постоянное хранилище, схему валидации и тесты. Контракт HTTP уже можно оставить тем же. Для закрепления откройте в REST API на Express, в нативном запуске TypeScript и в проверке JSON через Zod.
Как изучить тему в Кодике
Закрепите объекты, массивы, async/await и HTTP-статусы до подключения базы. Сначала восстановите главную функцию и проверьте её на трёх значениях.
| Шаг | Что разобрать | Мини-проверка |
|---|---|---|
| 1. Основа | Request, Response, URL и JSON в JavaScript | Объясните вход и выход функции. |
| 2. Инструмент | Bun.serve, методы HTTP и запуск TypeScript | Повторите команду по памяти. |
| 3. Проект | Три маршрута с проверкой входа | Проверьте два сценария. |
| 4. Рефакторинг | Разделение router, service и storage | Разделите ввод, логику и вывод. |
Попробуйте вызвать API не только curl, но и fetch из маленькой HTML-страницы. Базовый синтаксис закрепите через курс JavaScript.
Клиент не отличает создание от ошибки. Используйте 201, 400, 404 и 500 по смыслу.
Тело может быть невалидным JSON. Обрабатывайте ошибку чтения отдельно.
Массив очищается при перезапуске. Подпишите это ограничение в README.
С ростом маршрутов вынесите разбор пути и функции действий, иначе ветки станут нечитаемыми.
Что получится в итоге

Так выглядит законченный учебный проект: есть понятный вход, видимый результат, защита от неверных данных и место для следующего улучшения.
Нужен ли Express для Bun.serve?
Нет. Runtime уже предоставляет HTTP-сервер и стандартные веб-объекты.
Почему POST возвращает 201?
Запрос успешно создал новый ресурс, и этот статус точнее обычного 200.
Что проверять первым, если Bun REST API не запускается?
Версию среды, текущую папку, команду запуска и первую строку ошибки.
Как понять, что проект Bun REST API работает?
Проверьте основной сценарий, неверный ввод, повторный запуск и один граничный случай.
Можно ли начать с готового кода?
Да. Добейтесь результата, измените одно правило и восстановите ключевую функцию без копирования.
Выберите одно улучшение и проведите его от ввода до результата.
Если не хватает основы, перейдите курс JavaScript, затем восстановите ключевую функцию без подсказки.