- Nuxt
Nuxt server APIの作り方|NitroでCRUDとエラー応答を実測
Nuxtのserver APIとは、server/api/ にファイルを置くだけで /api/... のエンドポイントになる、Nuxt内蔵のAPIサーバー機能です。 中で動くのは Nitro(サーバーエンジン)と h3(軽量なHTTPライブラリ)で、フロントエンドと同じプロジェクトでAPIを作ってデプロイできます。
この記事では、Nuxt 4.5.2(Nitro 2.13.4、h3 1.15.11) で、ファイル名とURL・メソッドの対応、入力の受け取り方とzodでのチェック、エラー・Cookie・ミドルウェア・ストレージを早見表にまとめ、SQLiteを使ったCRUD一式をコピペで動く形で載せます。curl -i で送った 201・400・401・404・405・422・500 の実際の応答、useFetch の 型推論を型チェックの出力で確認し、未定義のメソッドでHTMLが返るという実測で見つけたつまずきも紹介します。
Nuxt server API 早見表
| やりたいこと | 書き方 | 例 |
|---|---|---|
/api/xxx を作る |
server/api/xxx.ts |
server/api/hello.ts → /api/hello |
/api なしのURLを作る |
server/routes/xxx.ts |
server/routes/health.get.ts → /health |
| メソッドを限定する | ファイル名に .get .post .patch .delete など |
tasks/index.post.ts → POST /api/tasks |
| URLの一部を受け取る | [id].ts + getRouterParam(event, 'id') |
/api/tasks/1 → '1' |
| クエリを受け取る | getQuery(event) |
?done=true → { done: 'true' } |
| JSONボディを受け取る | await readBody(event) |
— |
| 入力チェックつきで受け取る | readValidatedBody / getValidatedQuery / getValidatedRouterParams |
readValidatedBody(event, schema.parse) |
| ステータスコードを変える | setResponseStatus(event, 201) |
作成時は201 |
| エラーを返す | throw createError({ status: 404, statusText }) |
JSONのエラー応答になる |
| Cookieを読む・書く | getCookie / setCookie / deleteCookie |
ログイン状態の保持 |
| 全リクエストの前に処理 | server/middleware/xxx.ts |
ログ、リクエストID |
| データを保存する | useStorage('名前') |
メモリ・ファイル・Redisなど |
| クライアントから呼ぶ | useFetch / $fetch |
戻り値の型が自動で付く |
- ハンドラーは
defineEventHandler((event) => { ... })で書き、returnした値がレスポンスになります(オブジェクトならJSON)。 server/utils/の関数と h3 のユーティリティは自動インポートされます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Nuxt server APIの作り方|NitroでCRUDとエラー応答を実測」
検証環境
| 項目 | バージョン |
|---|---|
| Nuxt | 4.5.2 |
| Nitro / h3 | 2.13.4 / 1.15.11 |
| zod | 4.6.5 |
| Node.js | 22.22.2(標準の node:sqlite を使用) |
npx nuxt build でビルドし、PORT=7452 node .output/server/index.mjs で起動したサーバーに curl を送りました。
server/ ディレクトリの役割
server/
├── api/tasks/index.get.ts GET /api/tasks
├── api/tasks/index.post.ts POST /api/tasks
├── api/tasks/[id].get.ts GET /api/tasks/:id(patch・delete も同様)
├── api/tasks/[id].ts それ以外のメソッド(405を返す)
├── api/[...path].ts どれにも一致しない /api/**(404を返す)
├── routes/health.get.ts GET /health(/api なし)
├── middleware/request-id.ts 全リクエストの前に実行
├── utils/db.ts など 自動インポートされる共通関数
└── plugins/ 起動時に1回実行(初期化など)
server/api/ と server/routes/ の違いは「URLの先頭に /api が付くかどうか」だけで、書き方は同じです。
CRUD完成コード(SQLite+zod)
タスクを登録・一覧・取得・更新・削除するAPIです。Node.js 22 標準の node:sqlite を使うので、DBドライバーの追加は不要です(起動時に ExperimentalWarning: SQLite is an experimental feature が1回出ます)。
npm install nuxt@4.5.2 zod
nuxt.config.ts
// nuxt.config.ts(Nuxt 4.5.2)
export default defineNuxtConfig({
compatibilityDate: '2026-09-01',
devtools: { enabled: false },
runtimeConfig: {
apiToken: 'dev-token', // 本番は NUXT_API_TOKEN で上書き
},
nitro: {
storage: {
// useStorage('notes') をファイル(./data/notes)に保存
notes: { driver: 'fs', base: './data/notes' },
},
},
})
server/utils(DB・入力ルール・共通関数)
// server/utils/db.ts — Node.js 22 標準の node:sqlite を使う
import { DatabaseSync } from 'node:sqlite'
import { mkdirSync } from 'node:fs'
export type Task = { id: number; title: string; done: boolean; createdAt: string }
let db: DatabaseSync | undefined
export function useDb() {
if (!db) {
mkdirSync('./data', { recursive: true })
db = new DatabaseSync('./data/app.db')
db.exec(`CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)`)
}
return db
}
// DBの行 → APIで返す形
export function toTask(row: any): Task {
return { id: row.id, title: row.title, done: row.done === 1, createdAt: row.created_at }
}
// server/utils/schemas.ts — 入力チェックのルール(zod 4)
import { z } from 'zod'
export const idParamSchema = z.object({
id: z.coerce.number().int().positive(),
})
export const createTaskSchema = z.object({
title: z.string().trim().min(1, 'titleは必須です').max(100),
})
export const updateTaskSchema = z
.object({
title: z.string().trim().min(1).max(100).optional(),
done: z.boolean().optional(),
})
.refine((v) => v.title !== undefined || v.done !== undefined, '更新する項目がありません')
export const listQuerySchema = z.object({
done: z.enum(['true', 'false']).optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
})
// server/utils/validate.ts — zodのエラーを422で返す共通関数
import type { H3Event } from 'h3'
import type { z } from 'zod'
export async function readBodyAs<T extends z.ZodType>(event: H3Event, schema: T): Promise<z.infer<T>> {
const body = await readBody(event).catch(() => {
throw createError({ status: 400, statusText: 'Invalid JSON' })
})
const result = schema.safeParse(body)
if (!result.success) {
throw createError({
status: 422,
statusText: 'Unprocessable Entity',
data: result.error.issues.map((i) => ({ path: i.path.join('.'), message: i.message })),
})
}
return result.data
}
server/api/tasks(エンドポイント)
// server/api/tasks/index.get.ts — GET /api/tasks?done=false&limit=10
export default defineEventHandler(async (event) => {
const { done, limit } = await getValidatedQuery(event, listQuerySchema.parse)
const db = useDb()
const rows = done === undefined
? db.prepare('SELECT * FROM tasks ORDER BY id LIMIT ?').all(limit)
: db.prepare('SELECT * FROM tasks WHERE done = ? ORDER BY id LIMIT ?').all(done === 'true' ? 1 : 0, limit)
return rows.map(toTask)
})
// server/api/tasks/index.post.ts — POST /api/tasks { "title": "..." }
export default defineEventHandler(async (event) => {
const { title } = await readBodyAs(event, createTaskSchema)
const db = useDb()
const { lastInsertRowid } = db.prepare('INSERT INTO tasks (title) VALUES (?)').run(title)
const task = toTask(db.prepare('SELECT * FROM tasks WHERE id = ?').get(lastInsertRowid))
setResponseStatus(event, 201)
setResponseHeader(event, 'Location', `/api/tasks/${task.id}`)
return task
})
// server/api/tasks/[id].get.ts — GET /api/tasks/:id
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, idParamSchema.parse)
const row = useDb().prepare('SELECT * FROM tasks WHERE id = ?').get(id)
if (!row) {
throw createError({ status: 404, statusText: 'Task Not Found', data: { id } })
}
return toTask(row)
})
// server/api/tasks/[id].patch.ts — PATCH /api/tasks/:id { "title"?: "...", "done"?: true }
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, idParamSchema.parse)
const input = await readBodyAs(event, updateTaskSchema)
const db = useDb()
const current = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id)
if (!current) throw createError({ status: 404, statusText: 'Task Not Found', data: { id } })
const next = { ...toTask(current), ...input }
db.prepare('UPDATE tasks SET title = ?, done = ? WHERE id = ?').run(next.title, next.done ? 1 : 0, id)
return next
})
// server/api/tasks/[id].delete.ts — DELETE /api/tasks/:id(トークン必須)
export default defineEventHandler(async (event) => {
// Authorization: Bearer <token> を確認(トークンは runtimeConfig から)
if (getRequestHeader(event, 'authorization') !== `Bearer ${useRuntimeConfig(event).apiToken}`) {
throw createError({ status: 401, statusText: 'Unauthorized' })
}
const { id } = await getValidatedRouterParams(event, idParamSchema.parse)
const { changes } = useDb().prepare('DELETE FROM tasks WHERE id = ?').run(id)
if (changes === 0) throw createError({ status: 404, statusText: 'Task Not Found', data: { id } })
setResponseStatus(event, 204)
return null
})
// server/api/tasks/[id].ts — GET/PATCH/DELETE 以外のメソッドを405にする
export default defineEventHandler((event) => {
setResponseHeader(event, 'Allow', 'GET, PATCH, DELETE')
throw createError({ status: 405, statusText: 'Method Not Allowed' })
})
メソッドつきのファイル([id].get.ts)が優先され、メソッドなしの [id].ts は残りのメソッドを受け取ります。 この組み合わせで、未定義のメソッドに405を返せました(後述の [...path].ts とあわせて使います)。
curl -i で実際の応答を確認
共通ヘッダー等は省略しています。
201 Created(作成)
$ curl -i -X POST localhost:7452/api/tasks -H 'Content-Type: application/json' -d '{"title":"備品を発注する"}'
HTTP/1.1 201 Created
Location: /api/tasks/1
content-type: application/json
Content-Length: 87
{"id":1,"title":"備品を発注する","done":false,"createdAt":"2026-09-29 10:41:46"}
200 OK(一覧・更新)
$ curl -s localhost:7452/api/tasks
[{"id":1,"title":"備品を発注する",...},{"id":2,"title":"議事録をまとめる",...}]
$ curl -i -X PATCH localhost:7452/api/tasks/1 -H 'Content-Type: application/json' -d '{"done":true}'
HTTP/1.1 200 OK
content-type: application/json
{"id":1,"title":"備品を発注する","done":true,"createdAt":"2026-09-29 10:41:46"}
422 Unprocessable Entity(入力内容のエラー)
$ curl -i -X POST localhost:7452/api/tasks -H 'Content-Type: application/json' -d '{"title":""}'
HTTP/1.1 422 Unprocessable Entity
content-type: application/json
cache-control: no-cache
{
"error": true,
"url": "http://localhost:7452/api/tasks",
"statusCode": 422,
"statusMessage": "Unprocessable Entity",
"message": "",
"data": [ { "path": "title", "message": "titleは必須です" } ]
}
400 Bad Request(JSONが壊れている・パラメーターが不正)
$ curl -i -X POST localhost:7452/api/tasks -H 'Content-Type: application/json' -d '{"title":'
HTTP/1.1 400 Invalid JSON
$ curl -i localhost:7452/api/tasks/abc
HTTP/1.1 400 Validation Error
content-type: application/json
{
"error": true,
"url": "http://localhost:7452/api/tasks/abc",
"statusCode": 400,
"statusMessage": "Validation Error",
"message": "[\n {\n \"expected\": \"number\",\n \"code\": \"invalid_type\",\n ...(zodのエラー全文)",
"data": { "name": "ZodError", "message": "(message と同じ内容)" }
}
404 / 401 / 204 / 500(本文は1行に要約)
$ curl -i localhost:7452/api/tasks/999
HTTP/1.1 404 Task Not Found
{ "error": true, "statusCode": 404, "statusMessage": "Task Not Found", "message": "", "data": { "id": 999 }, ... }
$ curl -i -X DELETE localhost:7452/api/tasks/2
HTTP/1.1 401 Unauthorized
{ "error": true, "statusCode": 401, "statusMessage": "Unauthorized", "message": "", ... }
$ curl -i -X DELETE localhost:7452/api/tasks/2 -H 'Authorization: Bearer dev-token'
HTTP/1.1 204 No Content
$ curl -i localhost:7452/api/boom
HTTP/1.1 500 Server Error
{ "error": true, "statusCode": 500, "statusMessage": "Server Error", "message": "Server Error", ... }
/api/boom は throw new Error('DB connection lost (secret detail)') を投げるハンドラーです。想定外の例外は500になり、エラー文(secret detail)はレスポンスに含まれませんでした。
入力チェック:h3の既定(400)と自作(422)の違い
h3 の readValidatedBody・getValidatedQuery・getValidatedRouterParams は、第2引数に zod の schema.parse を渡すだけで検証でき、戻り値にスキーマの型が付きます。ただし実測のとおり、既定のエラーは400で、zod のエラー全文が文字列として message に入ります。
| 比較 | h3の既定(schema.parse を渡す) |
自作(safeParse + createError) |
|---|---|---|
| ステータス | 400 Validation Error | 自由(今回は422) |
| エラーの形 | message に zod のJSON文字列 |
data に { path, message } の配列 |
フォームのAPIは自作の形、URLやクエリの形式チェックはh3の既定、と使い分けると扱いやすいです。
createError の書き方(status と statusCode)
Nuxt 4.x の公式ドキュメントは、次のNitro v3(h3 v2)で標準になる status / statusText で説明しています。従来の statusCode / statusMessage も動きます。
throw createError({ status: 404, statusText: 'Task Not Found', data: { id } }) // 新しい書き方
throw createError({ statusCode: 409, statusMessage: 'Conflict', message: '同じ名前のタスクがあります' }) // 従来の書き方
従来の書き方で投げると 409 Conflict、本文は "statusCode": 409, "statusMessage": "Conflict", "message": "同じ名前のタスクがあります" でした。どちらで書いても、レスポンスJSONのキーは statusCode / statusMessage です。message は指定しないと空文字になるので、利用者向けの説明は message か data に入れます。
ヘッダーとCookie
// server/api/login.post.ts — Cookieを発行(説明用の簡易版)
export default defineEventHandler(async (event) => {
const { name } = await readBody<{ name?: string }>(event)
if (!name) throw createError({ status: 400, statusText: 'name is required' })
setCookie(event, 'member', name, { httpOnly: true, sameSite: 'lax', path: '/', maxAge: 60 * 60 })
return { ok: true }
})
// server/api/me.get.ts では getCookie(event, 'member') で読み、なければ 401 を返す
$ curl -i -X POST localhost:7452/api/login -H 'Content-Type: application/json' -d '{"name":"sato"}'
HTTP/1.1 200 OK
set-cookie: member=sato; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax
$ curl -i localhost:7452/api/me -H 'Cookie: member=sato'
HTTP/1.1 200 OK
{"name":"sato","requestId":"af8c8e5f"}
Cookieなしの /api/me は 401 Not logged in でした。実際のログインでは、署名・暗号化されたセッション(useSession や nuxt-auth-utils など)を使います。
server/middleware:全リクエストの前に処理する
// server/middleware/request-id.ts — 全リクエストの前に動く
import { randomUUID } from 'node:crypto'
export default defineEventHandler((event) => {
const id = randomUUID().slice(0, 8)
event.context.requestId = id
setResponseHeader(event, 'X-Request-Id', id)
console.log(`[${id}] ${event.method} ${event.path}`)
// 何も return しない(return すると、そこで応答が終わる)
})
サーバーのログは次のようになりました。
[14359259] GET /api/notes/none
[a9421152] HEAD /api/tasks
[1eb66f6f] GET /api/tasks
最後の行は、ページのSSR中に useFetch('/api/tasks') が呼んだものです。サーバー内部からの呼び出しにもミドルウェアが動きます。event.context に入れた値は後のハンドラーで読めます。
useStorage:Nitroのストレージにデータを保存
小さなデータはDBなしで Nitroのストレージ(unstorage) に保存できます。notes は上の設定でファイル保存です。
// server/api/notes/[key].put.ts — PUT /api/notes/:key
export default defineEventHandler(async (event) => {
const key = getRouterParam(event, 'key')!
const body = await readBody<{ text: string }>(event)
await useStorage('notes').setItem(key, { text: body.text, savedAt: new Date().toISOString() })
return { key, saved: true }
})
// 読み出し([key].get.ts)は await useStorage('notes').getItem(key)、なければ null
$ curl -i -X PUT localhost:7452/api/notes/memo1 -H 'Content-Type: application/json' -d '{"text":"棚卸しは金曜"}'
HTTP/1.1 200 OK
{"key":"memo1","saved":true}
$ curl -i localhost:7452/api/notes/memo1
HTTP/1.1 200 OK
{"key":"memo1","value":{"text":"棚卸しは金曜","savedAt":"2026-09-29T10:41:58.611Z"}}
同じ内容が data/notes/memo1 にファイルとして保存されていました。driver を変えるだけで保存先を切り替えられます(fs、redis など)。設定していない名前はメモリ保存で、再起動すると消えます。一覧・絞り込みが多いデータはDB向きです。
クライアントから呼ぶ:useFetch と $fetch
<!-- app/app.vue -->
<script setup lang="ts">
const { data: tasks, refresh } = await useFetch('/api/tasks') // tasks は Task[] として扱える
const title = ref('')
async function add() {
await $fetch('/api/tasks', { method: 'POST', body: { title: title.value } })
title.value = ''
await refresh()
}
</script>
<template>
<ul><li v-for="t in tasks" :key="t.id">{{ t.id }}: {{ t.title }} {{ t.done ? '✓' : '' }}</li></ul>
<input v-model="title" /><button @click="add">追加</button>
</template>
useFetch は <script setup> の最上位で使い、SSRで取得したデータをHTMLに埋め込みます。ボタン操作で呼ぶときは $fetch です。SSRのHTMLにはタスクがすでに入っていました。
<ul><!--[--><li>1: 備品を発注する ✓</li><!--]--></ul>
型推論が効くことを nuxt typecheck で確認
Nuxtはビルド時に .nuxt/types/nitro-routes.d.ts を生成し、URLとメソッドごとにハンドラーの戻り値の型を登録します。わざと間違えたコードで npx nuxt typecheck を実行しました。
<script setup lang="ts">
const { data: tasks } = await useFetch('/api/tasks')
const first = tasks.value?.[0]
const t: string | undefined = first?.titel // ← わざと typo
const created = await $fetch('/api/tasks', { method: 'POST', body: { title: 'x' } })
const n: string = created.id // ← id は number
</script>
app/components/TypeCheck.vue(4,38): error TS2551: Property 'titel' does not exist on type 'SerializeObject<Task>'. Did you mean 'title'?
app/components/TypeCheck.vue(6,7): error TS2322: Type 'number' is not assignable to type 'string'.
サーバーの Task 型がクライアントに届き、打ち間違いがビルド前に見つかります。 型チェックは typescript@6 + vue-tsc で実行しました(TypeScript 7.0.2 では vue-tsc 3.3.11 が起動しませんでした)。
クライアント側のエラー処理
$fetch(中身は ofetch 1.5.1)はエラー応答で FetchError を投げます。422を受けたときの値は次のとおりでした。
e.statusCode → 422
e.data.data → [ { path: 'title', message: 'titleは必須です' } ]
e.message → [POST] "http://localhost:7452/api/tasks": 422 Unprocessable Entity
e.data がレスポンスのJSON全体、e.data.data が createError の data なので、フォームの項目ごとのエラー表示にそのまま使えます。
定義していないメソッドを送ると HTML が返る(実測で見つけたつまずき)
[id].ts(405用)を置く前は、PUT /api/tasks/1 に 200 OK と HTML(アプリの画面) が返りました。HEAD(curl -I)も同じで、.get.ts には一致せず、ページ描画に回されます(app/pages/ があるプロジェクトでは 404 Page not found)。
| 状態 | PUT /api/tasks/1 の結果 |
|---|---|
| メソッドつきのファイルだけ | 200 と HTML |
server/api/[...path].ts を追加 |
変わらず 200 と HTML |
server/api/tasks/[id].ts を追加 |
405 Method Not Allowed + Allow ヘッダー |
$ curl -i -X PUT localhost:7452/api/tasks/1
HTTP/1.1 405 Method Not Allowed
Allow: GET, PATCH, DELETE
content-type: application/json
存在しないパスは catch-all で404にします(追加後の GET /api/nothing は 404 API Not Found)。
// server/api/[...path].ts — どのAPIにも一致しない /api/** を404(JSON)にする
export default defineEventHandler((event) => {
throw createError({ status: 404, statusText: 'API Not Found', message: `${event.method} ${event.path} は存在しません` })
})
Nitro v3(Nuxt 5)への移行予定
Nuxt 5 は2026年9月時点で開発中(nightly版)で、Nitro v3・h3 v2 に移行します。 公式のアップグレードガイドで確認できた主な変更です。
| 項目 | Nuxt 4(現在) | Nuxt 5(予定) |
|---|---|---|
| ユーティリティの読み込み | 自動インポート | nuxt/server から import(defineEventHandler、getQuery、readBody など) |
| createError | statusCode / statusMessage も可 |
status / statusText(Web標準の名前) |
| リクエストの情報 | event.path、event.method |
event.url.pathname、event.req.method |
| 移行期間の互換 | — | nitroLegacy で Nitro v2 の書き方のまま段階的に移行可 |
今のうちに createError を status / statusText で書いておくと、移行時の書き換えが減ります。
Next.js Route Handlers との対応
| やりたいこと | Nuxt(server/api) | Next.js(Route Handlers) |
|---|---|---|
| ファイル | server/api/tasks/index.get.ts |
app/api/tasks/route.ts |
| メソッドの分け方 | ファイル名(.get・.post) |
関数名(export async function GET) |
| クライアントの型推論 | useFetch に自動で付く |
自分で型を共有する |
よくあるつまずきと対処
| 症状 | 原因 | 対処 |
|---|---|---|
| 未定義のメソッドで 200 と HTML が返る | ページ描画に回される | メソッドなしの [id].ts で405を返す |
curl -I で 404 や HTML になる |
.get.ts は HEAD に一致しない |
GETで確認(curl -s -D - -o /dev/null URL) |
nuxt typecheck が起動しない |
TypeScript 7 と vue-tsc 3.3 の組み合わせ | typescript@6 を使う(2026年9月の実測) |
よくある質問
Q. server/api と server/routes のどちらに置けばよいですか?
A. 画面から呼ぶJSON APIは server/api/、/health のように /api を付けたくないURLは server/routes/ です。
Q. 本番でDBを使うには?
A. node:sqlite は単一サーバー向けです。複数台では PostgreSQL などに接続し、server/utils/db.ts だけを差し替えます。
Q. メソッドごとにファイルを分けるべきですか?
A. 分けるとメソッドごとに型推論が効き、ファイル一覧がそのままAPI一覧になります。 未定義メソッド用に [id].ts で405を返しておくと安心です。
まとめ
server/api/にファイルを置くと/api/...のAPIになり、ファイル名の.get.postでメソッドを指定できるgetRouterParam・getQuery・readBodyで入力を受け取り、検証つき版には zod のparseを渡せる- h3既定の検証エラーは400で zod の全文入り。フォーム向けは
safeParseで422と項目別のdataに整える - 未定義メソッドはHTMLが返るため、メソッドなしの
[id].tsで405、[...path].tsで404を返す useFetch/$fetchは戻り値の型を自動で受け取り、typo をnuxt typecheckで検出できた- Nuxt 5 で Nitro v3・h3 v2 へ移行予定。
status/statusTextで書いておくと移行が楽になる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Nuxt server APIの作り方|NitroでCRUDとエラー応答を実測」
株式会社RJCでは、フロントエンドとAPIを1つのプロジェクトで組み立てる開発スタイルを、実際に動かして確かめながらプロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。